summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorm-stephen <truestyle2005@163.com>2019-07-04 17:02:13 +0800
committerGitHub <noreply@github.com>2019-07-04 17:02:13 +0800
commit225f3e4c10cbf7021bfc15ee0441e831ac108458 (patch)
tree555aee123c1dd91a430de1143326a4b179cef4e9
parent2e5b1c647170972fc265e47dac1326cd890f341a (diff)
parent16f1854a9ca98c00a5058c55e8a59df2c3463f4d (diff)
downloadqtlocation-mapboxgl-225f3e4c10cbf7021bfc15ee0441e831ac108458.tar.gz
Merge branch 'master' into Stephen-CJK
-rw-r--r--.gitignore1
-rw-r--r--.gitmodules9
-rw-r--r--CMakeLists.txt1
-rw-r--r--Makefile71
-rw-r--r--benchmark/storage/offline_database.benchmark.cpp73
-rw-r--r--ci.template223
-rw-r--r--circle.yml87
-rw-r--r--cloudformation/ci.template.js117
-rw-r--r--cmake/core.cmake7
-rw-r--r--cmake/mason-dependencies.cmake1
-rw-r--r--cmake/mbgl.cmake3
-rw-r--r--cmake/render-test.cmake29
-rw-r--r--cmake/sqlite.cmake11
-rw-r--r--cmake/vendor.cmake1
-rw-r--r--include/mbgl/storage/default_file_source.hpp65
-rw-r--r--include/mbgl/storage/resource_options.hpp15
-rw-r--r--include/mbgl/style/expression/expression.hpp3
-rw-r--r--include/mbgl/style/expression/number_format.hpp40
-rw-r--r--include/mbgl/style/layer_properties.hpp3
-rw-r--r--include/mbgl/util/platform.hpp3
-rw-r--r--package.json1
-rw-r--r--platform/android/CHANGELOG.md38
-rw-r--r--platform/android/MapboxGLAndroidSDK/gradle.properties2
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/camera/CameraUpdateFactory.java81
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java28
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java11
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/log/Logger.java117
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java13
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/http/HttpRequestImpl.java12
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineManager.java257
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegion.java75
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/storage/FileSource.java1
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/ColorUtils.java12
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt6
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentTest.kt104
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/log/LoggerTest.kt93
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/utils/ColorUtilsTest.kt22
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/build.gradle1
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/AppCenter.kt16
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/camera/CameraUpdateFactoryTest.kt144
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/integration/BaseIntegrationTest.kt3
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/BaseLayerTest.kt3
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/MapboxMapInstrumentationTest.kt41
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/MapboxTest.java4
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/NativeMapViewTest.kt3
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/VisibleRegionTest.kt66
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/activity/BaseTest.java6
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/fragment/MapDialogFragmentTest.kt3
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/ImageMissingTest.kt3
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/RemoveUnusedImagesTest.kt3
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/CacheTest.kt89
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/OfflineManagerTest.kt30
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/OfflineUtilsTest.java4
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/render/RenderTest.java4
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/storage/FileSourceMapTest.kt3
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/storage/FileSourceStandaloneTest.kt3
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/storage/FileSourceTestUtils.kt3
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml11
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/assets/points-sf.geojson115
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/LatLngBoundsActivity.java172
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/LatLngBoundsActivity.kt167
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/LocationModesActivity.java10
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/DeleteRegionActivity.java24
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/storage/CacheManagementActivity.kt79
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/SymbolLayerActivity.java9
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_arrow_upward.xml9
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_cache_management.xml32
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_latlngbounds.xml63
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml1
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml1
-rw-r--r--platform/android/core-files.json1
-rw-r--r--platform/android/gradle/dependencies.gradle4
-rw-r--r--platform/android/scripts/exclude-activity-gen.json3
-rwxr-xr-xplatform/android/src/jni.cpp2
-rwxr-xr-xplatform/android/src/native_map_view.cpp2
-rw-r--r--platform/android/src/offline/offline_manager.cpp98
-rw-r--r--platform/android/src/offline/offline_manager.hpp15
-rw-r--r--platform/android/src/offline/offline_region.cpp36
-rw-r--r--platform/android/src/offline/offline_region.hpp11
-rw-r--r--platform/android/src/text/format_number.cpp81
-rw-r--r--platform/android/src/text/format_number_jni.hpp29
-rw-r--r--platform/darwin/src/MGLAccountManager.h2
-rw-r--r--platform/darwin/src/MGLOfflineStorage.h16
-rw-r--r--platform/darwin/src/MGLStyle.h27
-rw-r--r--platform/darwin/src/run_loop.cpp2
-rw-r--r--platform/darwin/src/string_nsstring.mm23
-rw-r--r--platform/darwin/test/MGLClockDirectionFormatterTests.m7
-rw-r--r--platform/darwin/test/MGLCompassDirectionFormatterTests.m5
-rw-r--r--platform/darwin/test/MGLCoordinateFormatterTests.m5
-rw-r--r--platform/darwin/test/MGLMapViewTests.m117
-rw-r--r--platform/darwin/test/MGLNSStringAdditionsTests.m2
-rw-r--r--platform/darwin/test/MGLOfflineStorageTests.mm80
-rw-r--r--platform/default/include/mbgl/storage/offline_database.hpp14
-rw-r--r--platform/default/src/mbgl/gfx/headless_frontend.cpp13
-rw-r--r--platform/default/src/mbgl/storage/default_file_source.cpp53
-rw-r--r--platform/default/src/mbgl/storage/file_source.cpp2
-rw-r--r--platform/default/src/mbgl/storage/offline_database.cpp131
-rw-r--r--platform/default/src/mbgl/util/format_number.cpp35
-rw-r--r--platform/glfw/glfw_view.cpp2
-rw-r--r--platform/glfw/glfw_view.hpp4
-rw-r--r--platform/glfw/main.cpp2
-rw-r--r--platform/ios/CHANGELOG.md41
-rw-r--r--platform/ios/DEVELOPING.md19
-rw-r--r--platform/ios/Integration Tests/Annotation Tests/MGLAnnotationViewIntegrationTests.m37
-rw-r--r--platform/ios/Integration Tests/MGLCameraTransitionTests.mm2
-rw-r--r--platform/ios/Mapbox-iOS-SDK-snapshot-dynamic.podspec2
-rw-r--r--platform/ios/Mapbox-iOS-SDK-static-part.podspec14
-rw-r--r--platform/ios/Mapbox-iOS-SDK-stripped.podspec2
-rw-r--r--platform/ios/Mapbox-iOS-SDK.podspec2
-rw-r--r--platform/ios/app/MBXState.m17
-rw-r--r--platform/ios/app/MBXStateManager.m11
-rw-r--r--platform/ios/app/MBXViewController.m8
-rw-r--r--platform/ios/ios.xcodeproj/project.pbxproj3
-rwxr-xr-xplatform/ios/scripts/lint-podspecs.js79
-rwxr-xr-xplatform/ios/scripts/package.sh28
-rw-r--r--platform/ios/src/MGLMapView+Impl.h1
-rw-r--r--platform/ios/src/MGLMapView+Impl.mm5
-rw-r--r--platform/ios/src/MGLMapView.h227
-rw-r--r--platform/ios/src/MGLMapView.mm213
-rw-r--r--platform/ios/src/MGLMapViewDelegate.h14
-rw-r--r--platform/ios/src/MGLMapView_Private.h1
-rw-r--r--platform/ios/test/MGLAnnotationViewTests.m6
-rw-r--r--platform/ios/test/MGLMapAccessibilityElementTests.m5
-rw-r--r--platform/ios/test/MGLMapViewDelegateIntegrationTests.swift2
-rw-r--r--platform/linux/config.cmake14
-rw-r--r--platform/macos/CHANGELOG.md8
-rw-r--r--platform/macos/DEVELOPING.md4
-rw-r--r--platform/macos/macos.xcodeproj/project.pbxproj2
-rw-r--r--platform/macos/src/MGLMapView+Impl.h1
-rw-r--r--platform/macos/src/MGLMapView+Impl.mm5
-rw-r--r--platform/macos/src/MGLMapView.h114
-rw-r--r--platform/macos/src/MGLMapView.mm72
-rw-r--r--platform/macos/src/MGLMapViewDelegate.h14
-rw-r--r--platform/macos/src/MGLMapView_Private.h1
-rw-r--r--platform/macos/test/MGLMapViewDelegateIntegrationTests.swift1
-rw-r--r--platform/node/CHANGELOG.md1
-rw-r--r--platform/node/README.md2
-rw-r--r--platform/node/src/node_expression.cpp3
-rw-r--r--platform/node/src/node_map.cpp28
-rw-r--r--platform/node/src/node_map.hpp1
-rw-r--r--platform/node/src/node_request.cpp65
-rw-r--r--platform/node/src/node_request.hpp26
-rw-r--r--platform/node/test/ignores.json1
-rw-r--r--platform/qt/qt.cmake1
-rw-r--r--platform/qt/src/format_number.cpp26
-rw-r--r--render-test/filesystem.hpp9
-rw-r--r--render-test/main.cpp154
-rw-r--r--render-test/metadata.hpp52
-rw-r--r--render-test/parser.cpp572
-rw-r--r--render-test/parser.hpp34
-rw-r--r--render-test/runner.cpp418
-rw-r--r--render-test/runner.hpp28
-rw-r--r--scripts/config.xcconfig.in2
-rwxr-xr-xscripts/generate-file-lists.js1
-rw-r--r--src/core-files.json5
-rw-r--r--src/mbgl/renderer/layers/render_heatmap_layer.cpp4
-rw-r--r--src/mbgl/renderer/layers/render_heatmap_layer.hpp2
-rw-r--r--src/mbgl/renderer/layers/render_line_layer.cpp41
-rw-r--r--src/mbgl/renderer/layers/render_line_layer.hpp3
-rw-r--r--src/mbgl/renderer/layers/render_symbol_layer.cpp2
-rw-r--r--src/mbgl/renderer/paint_parameters.cpp14
-rw-r--r--src/mbgl/renderer/paint_parameters.hpp9
-rw-r--r--src/mbgl/renderer/paint_property_binder.hpp12
-rw-r--r--src/mbgl/renderer/render_layer.cpp26
-rw-r--r--src/mbgl/renderer/render_layer.hpp4
-rw-r--r--src/mbgl/renderer/render_orchestrator.cpp635
-rw-r--r--src/mbgl/renderer/render_orchestrator.hpp118
-rw-r--r--src/mbgl/renderer/render_pass.hpp1
-rw-r--r--src/mbgl/renderer/render_tree.hpp78
-rw-r--r--src/mbgl/renderer/renderer.cpp24
-rw-r--r--src/mbgl/renderer/renderer_impl.cpp647
-rw-r--r--src/mbgl/renderer/renderer_impl.hpp106
-rw-r--r--src/mbgl/renderer/tile_pyramid.cpp7
-rw-r--r--src/mbgl/storage/resource_options.cpp10
-rw-r--r--src/mbgl/style/expression/coercion.cpp2
-rw-r--r--src/mbgl/style/expression/number_format.cpp215
-rw-r--r--src/mbgl/style/expression/parsing_context.cpp2
-rw-r--r--src/mbgl/style/layers/background_layer_properties.cpp4
-rw-r--r--src/mbgl/style/layers/background_layer_properties.hpp2
-rw-r--r--src/mbgl/style/layers/circle_layer_properties.cpp4
-rw-r--r--src/mbgl/style/layers/circle_layer_properties.hpp2
-rw-r--r--src/mbgl/style/layers/fill_extrusion_layer_properties.cpp4
-rw-r--r--src/mbgl/style/layers/fill_extrusion_layer_properties.hpp2
-rw-r--r--src/mbgl/style/layers/fill_layer_properties.cpp4
-rw-r--r--src/mbgl/style/layers/fill_layer_properties.hpp2
-rw-r--r--src/mbgl/style/layers/heatmap_layer_properties.cpp4
-rw-r--r--src/mbgl/style/layers/heatmap_layer_properties.hpp2
-rw-r--r--src/mbgl/style/layers/hillshade_layer_properties.cpp4
-rw-r--r--src/mbgl/style/layers/hillshade_layer_properties.hpp2
-rw-r--r--src/mbgl/style/layers/layer_properties.cpp.ejs4
-rw-r--r--src/mbgl/style/layers/layer_properties.hpp.ejs2
-rw-r--r--src/mbgl/style/layers/line_layer_properties.cpp4
-rw-r--r--src/mbgl/style/layers/line_layer_properties.hpp2
-rw-r--r--src/mbgl/style/layers/raster_layer_properties.cpp4
-rw-r--r--src/mbgl/style/layers/raster_layer_properties.hpp2
-rw-r--r--src/mbgl/style/layers/symbol_layer_properties.cpp4
-rw-r--r--src/mbgl/style/layers/symbol_layer_properties.hpp2
-rw-r--r--src/mbgl/style/properties.hpp22
-rw-r--r--src/mbgl/tile/geometry_tile.cpp5
-rw-r--r--src/mbgl/tile/geometry_tile.hpp2
-rw-r--r--src/mbgl/tile/tile.hpp10
-rw-r--r--src/mbgl/util/i18n.cpp3
-rw-r--r--test/fixtures/expression_equality/number-format-currency.a.json9
-rw-r--r--test/fixtures/expression_equality/number-format-currency.b.json9
-rw-r--r--test/fixtures/expression_equality/number-format-default.a.json5
-rw-r--r--test/fixtures/expression_equality/number-format-default.b.json5
-rw-r--r--test/fixtures/expression_equality/number-format-precision.a.json10
-rw-r--r--test/fixtures/expression_equality/number-format-precision.b.json10
-rw-r--r--test/fixtures/local_glyphs/mixed.json16
-rw-r--r--test/fixtures/local_glyphs/no_local/expected.pngbin8579 -> 6963 bytes
-rw-r--r--test/fixtures/local_glyphs/no_local_with_content_insets/expected.pngbin8428 -> 6784 bytes
-rw-r--r--test/fixtures/local_glyphs/no_local_with_content_insets_and_pitch/expected.pngbin7263 -> 6026 bytes
-rw-r--r--test/fixtures/local_glyphs/ping_fang/expected.pngbin15747 -> 18019 bytes
-rw-r--r--test/storage/offline_database.test.cpp375
-rw-r--r--test/style/expression/expression.test.cpp4
m---------vendor/boost0
-rw-r--r--vendor/boost-files.json5
m---------vendor/filesystem0
-rw-r--r--vendor/filesystem-files.json13
-rw-r--r--vendor/filesystem.cmake5
m---------vendor/mapbox-gl-styles0
m---------vendor/mvt-fixtures0
222 files changed, 6590 insertions, 1929 deletions
diff --git a/.gitignore b/.gitignore
index 63499e2b30..e5a7cd9d21 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,7 @@ xcuserdata
/node_modules
/platform/ios/benchmark/assets/glyphs/DIN*
/platform/ios/benchmark/assets/tiles/mapbox.mapbox-terrain-v2,mapbox.mapbox-streets-v6
+/platform/darwin/developer.xcconfig
**/token
/platform/macos/macos.xcworkspace/xcshareddata/macos.xcscmblueprint
/platform/ios/ios.xcworkspace/xcshareddata/ios.xcscmblueprint
diff --git a/.gitmodules b/.gitmodules
index aeb2663b84..486a1c8bdc 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -94,3 +94,12 @@
[submodule "vendor/args"]
path = vendor/args
url = https://github.com/Taywee/args
+[submodule "vendor/mvt-fixtures"]
+ path = vendor/mvt-fixtures
+ url = https://github.com/mapbox/mvt-fixtures.git
+[submodule "vendor/mapbox-gl-styles"]
+ path = vendor/mapbox-gl-styles
+ url = https://github.com/mapbox/mapbox-gl-styles.git
+[submodule "vendor/filesystem"]
+ path = vendor/filesystem
+ url = https://github.com/gulrak/filesystem.git
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ca33c36be2..df2e4940fd 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -184,6 +184,7 @@ include(cmake/core.cmake)
if(COMMAND mbgl_platform_test)
include(cmake/test.cmake)
+ include(cmake/render-test.cmake)
endif()
if(COMMAND mbgl_platform_benchmark)
diff --git a/Makefile b/Makefile
index 63a3a20d33..aefd0b0458 100644
--- a/Makefile
+++ b/Makefile
@@ -213,10 +213,61 @@ IOS_XCODEBUILD_SIM = xcodebuild \
ARCHS=x86_64 ONLY_ACTIVE_ARCH=YES \
-derivedDataPath $(IOS_OUTPUT_PATH) \
-configuration $(BUILDTYPE) -sdk iphonesimulator \
- -destination 'platform=iOS Simulator,name=iPhone 6,OS=latest' \
-workspace $(IOS_WORK_PATH) \
-jobs $(JOBS)
+ifneq ($(MORE_SIMULATORS),)
+ IOS_LATEST = true
+ IOS_11 = true
+ IOS_10 = true
+ IOS_9 = true
+endif
+
+ifdef IOS_LATEST
+ IOS_XCODEBUILD_SIM += \
+ -destination 'platform=iOS Simulator,OS=latest,name=iPhone 8' \
+ -destination 'platform=iOS Simulator,OS=latest,name=iPhone Xs Max' \
+ -destination 'platform=iOS Simulator,OS=latest,name=iPhone Xr' \
+ -destination 'platform=iOS Simulator,OS=latest,name=iPad Pro (11-inch)'
+endif
+
+ifdef IOS_11
+ IOS_XCODEBUILD_SIM += \
+ -destination 'platform=iOS Simulator,OS=11.4,name=iPhone 7' \
+ -destination 'platform=iOS Simulator,OS=11.4,name=iPhone X' \
+ -destination 'platform=iOS Simulator,OS=11.4,name=iPad (5th generation)'
+endif
+
+ifdef IOS_10
+ IOS_XCODEBUILD_SIM += \
+ -destination 'platform=iOS Simulator,OS=10.3.1,name=iPhone SE' \
+ -destination 'platform=iOS Simulator,OS=10.3.1,name=iPhone 7 Plus' \
+ -destination 'platform=iOS Simulator,OS=10.3.1,name=iPad Pro (9.7-inch)'
+endif
+
+ifdef IOS_9
+ IOS_XCODEBUILD_SIM += \
+ -destination 'platform=iOS Simulator,OS=9.3,name=iPhone 6s Plus' \
+ -destination 'platform=iOS Simulator,OS=9.3,name=iPhone 6s' \
+ -destination 'platform=iOS Simulator,OS=9.3,name=iPad Air 2'
+endif
+
+# If IOS_XCODEBUILD_SIM does not contain a simulator destination, add the default.
+ifeq (, $(findstring destination, $(IOS_XCODEBUILD_SIM)))
+ IOS_XCODEBUILD_SIM += \
+ -destination 'platform=iOS Simulator,OS=latest,name=iPhone 8'
+else
+ IOS_XCODEBUILD_SIM += -parallel-testing-enabled YES
+endif
+
+ifneq ($(ONLY_TESTING),)
+ IOS_XCODEBUILD_SIM += -only-testing:$(ONLY_TESTING)
+endif
+
+ifneq ($(SKIP_TESTING),)
+ IOS_XCODEBUILD_SIM += -skip-testing:$(SKIP_TESTING)
+endif
+
ifneq ($(CI),)
IOS_XCODEBUILD_SIM += -xcconfig platform/darwin/ci.xcconfig
endif
@@ -241,10 +292,14 @@ iproj: $(IOS_PROJ_PATH)
xed $(IOS_WORK_PATH)
.PHONY: ios-lint
-ios-lint:
+ios-lint: ios-pod-lint
find platform/ios/framework -type f -name '*.plist' | xargs plutil -lint
find platform/ios/app -type f -name '*.plist' | xargs plutil -lint
+.PHONY: ios-pod-lint
+ios-pod-lint:
+ ./platform/ios/scripts/lint-podspecs.js
+
.PHONY: ios-test
ios-test: $(IOS_PROJ_PATH)
set -o pipefail && $(IOS_XCODEBUILD_SIM) -scheme 'CI' test $(XCPRETTY)
@@ -265,6 +320,12 @@ ios-sanitize-address: $(IOS_PROJ_PATH)
ios-static-analyzer: $(IOS_PROJ_PATH)
set -o pipefail && $(IOS_XCODEBUILD_SIM) analyze -scheme 'CI' test $(XCPRETTY)
+.PHONY: ios-install-simulators
+ios-install-simulators:
+ xcversion simulators --install="iOS 11.4" || true
+ xcversion simulators --install="iOS 10.3.1" || true
+ xcversion simulators --install="iOS 9.3" || true
+
.PHONY: ios-check-events-symbols
ios-check-events-symbols:
./platform/ios/scripts/check-events-symbols.sh
@@ -495,12 +556,6 @@ qt-docs:
.PHONY: test-node
test-node: node
npm test
- npm run test-suite
-
-.PHONY: test-node-recycle-map
-test-node-recycle-map: node
- npm test
- npm run test-render -- --recycle-map --shuffle
npm run test-query
npm run test-expressions
diff --git a/benchmark/storage/offline_database.benchmark.cpp b/benchmark/storage/offline_database.benchmark.cpp
index cfb1e4c0f2..afdf6db4fc 100644
--- a/benchmark/storage/offline_database.benchmark.cpp
+++ b/benchmark/storage/offline_database.benchmark.cpp
@@ -5,6 +5,7 @@
#include <mbgl/storage/response.hpp>
#include <mbgl/storage/sqlite3.hpp>
#include <mbgl/util/string.hpp>
+#include <mbgl/util/logging.hpp>
#include <random>
@@ -26,7 +27,7 @@ public:
void resetAmbientTiles() {
using namespace mbgl;
- db.clearTileCache();
+ db.clearAmbientCache();
for (unsigned i = 0; i < tileCount; ++i) {
const Resource ambient = Resource::tile("mapbox://tile_ambient" + util::toString(i), 1, 0, 0, 0, Tileset::Scheme::XYZ);
@@ -82,13 +83,13 @@ BENCHMARK_F(OfflineDatabase, InsertTileRegion)(benchmark::State& state) {
db.putRegionResource(regionID, offline, response);
}
}
-BENCHMARK_F(OfflineDatabase, InvalidateTileCache)(benchmark::State& state) {
+BENCHMARK_F(OfflineDatabase, InvalidateAmbientCache)(benchmark::State& state) {
while (state.KeepRunning()) {
- db.invalidateTileCache();
+ db.invalidateAmbientCache();
}
}
-BENCHMARK_F(OfflineDatabase, ClearTileCache)(benchmark::State& state) {
+BENCHMARK_F(OfflineDatabase, ClearAmbientCache)(benchmark::State& state) {
while (state.KeepRunning()) {
resetAmbientTiles();
}
@@ -129,3 +130,67 @@ BENCHMARK_F(OfflineDatabase, GetTile)(benchmark::State& state) {
assert(res != nullopt);
}
}
+
+BENCHMARK_F(OfflineDatabase, AddTilesToFullDatabase)(benchmark::State& state) {
+ using namespace mbgl;
+
+ Log::setObserver(std::make_unique<Log::NullObserver>());
+ db.setMaximumAmbientCacheSize(50 * 1024 * 5);
+
+ while (state.KeepRunning()) {
+ const Resource ambient = Resource::tile("mapbox://AddTilesToFullDatabase" +
+ util::toString(state.iterations()), 1, 0, 0, 0, Tileset::Scheme::XYZ);
+ db.put(ambient, response);
+ }
+
+ Log::removeObserver();
+}
+
+BENCHMARK_F(OfflineDatabase, AddTilesToDisabledDatabase)(benchmark::State& state) {
+ using namespace mbgl;
+
+ auto regions = db.listRegions().value();
+ if (!regions.empty()) {
+ db.deleteRegion(std::move(regions[0]));
+ }
+
+ // Disables the ambient cache.
+ db.setMaximumAmbientCacheSize(0);
+
+ while (state.KeepRunning()) {
+ const Resource ambient = Resource::tile("mapbox://AddTilesToFullDatabase" +
+ util::toString(state.iterations()), 1, 0, 0, 0, Tileset::Scheme::XYZ);
+ db.put(ambient, response);
+ }
+}
+
+BENCHMARK_F(OfflineDatabase, GetTileFromDisabledDatabase)(benchmark::State& state) {
+ using namespace mbgl;
+
+ auto regions = db.listRegions().value();
+ if (!regions.empty()) {
+ db.deleteRegion(std::move(regions[0]));
+ }
+
+ // Disables the ambient cache.
+ db.setMaximumAmbientCacheSize(0);
+
+ while (state.KeepRunning()) {
+ auto res = db.get(Resource::tile("mapbox://tile_ambient", 1, 0, 0, 0, Tileset::Scheme::XYZ));
+ assert(res == nullopt);
+ }
+}
+
+BENCHMARK_F(OfflineDatabase, ResizeDatabase)(benchmark::State& state) {
+ uint64_t size = 25 * 1024 * 1024;
+
+ while (state.KeepRunning()) {
+ db.setMaximumAmbientCacheSize(size);
+
+ size -= response.data->size();
+
+ if (size < response.data->size()) {
+ size = 25 * 1024 * 1024;
+ }
+ }
+}
diff --git a/ci.template b/ci.template
deleted file mode 100644
index 578ce24c81..0000000000
--- a/ci.template
+++ /dev/null
@@ -1,223 +0,0 @@
-{
- "AWSTemplateFormatVersion": "2010-09-09",
- "Description": "mapbox-gl-native travis resources",
- "Resources": {
- "BuildUser": {
- "Type": "AWS::IAM::User",
- "Properties": {
- "Policies": [
- {
- "PolicyName": "list-testing",
- "PolicyDocument": {
- "Statement": [
- {
- "Action": [
- "s3:ListBucket"
- ],
- "Effect": "Allow",
- "Resource": [
- "arn:aws:s3:::mapbox"
- ],
- "Condition": {
- "StringLike": {
- "s3:prefix": "mapbox-gl-native/*"
- }
- }
- }
- ]
- }
- },
- {
- "PolicyName": "build-testing",
- "PolicyDocument": {
- "Statement": [
- {
- "Action": [
- "s3:DeleteObject",
- "s3:GetObject",
- "s3:GetObjectAcl",
- "s3:PutObject",
- "s3:PutObjectAcl"
- ],
- "Effect": "Allow",
- "Resource": [
- "arn:aws:s3:::mapbox/mapbox-gl-native/*"
- ]
- }
- ]
- }
- },
- {
- "PolicyName": "list-node",
- "PolicyDocument": {
- "Statement": [
- {
- "Action": [
- "s3:ListBucket"
- ],
- "Resource": [
- "arn:aws:s3:::mapbox-node-binary"
- ],
- "Effect": "Allow"
- }
- ]
- }
- },
- {
- "PolicyName": "build-node",
- "PolicyDocument": {
- "Statement": [
- {
- "Action": [
- "s3:DeleteObject",
- "s3:GetObject",
- "s3:GetObjectAcl",
- "s3:PutObject",
- "s3:PutObjectAcl"
- ],
- "Resource": [
- "arn:aws:s3:::mapbox-node-binary/@mapbox/mapbox-gl-native/*"
- ],
- "Effect": "Allow"
- }
- ]
- }
- },
- {
- "PolicyName": "cloudwatch-metrics",
- "PolicyDocument": {
- "Statement": [
- {
- "Action": [
- "cloudwatch:PutMetricData",
- "cloudwatch:GetMetricData",
- "cloudwatch:GetMetricStatistics"
- ],
- "Effect": "Allow",
- "Resource": [
- "*"
- ]
- }
- ]
- }
- },
- {
- "PolicyName": "get-signing-key",
- "PolicyDocument": {
- "Statement": [
- {
- "Action": [
- "s3:GetObject"
- ],
- "Effect": "Allow",
- "Resource": [
- "arn:aws:s3:::mapbox/android/signing-credentials/secring.gpg"
- ]
- }
- ]
- }
- },
- {
- "PolicyName": "publish-metrics",
- "PolicyDocument": {
- "Statement": [
- {
- "Action": [
- "s3:PutObject",
- "s3:GetObject",
- "s3:GetObjectAcl"
- ],
- "Effect": "Allow",
- "Resource": [
- "arn:aws:s3:::mapbox-loading-dock/raw/mobile.binarysize/*",
- "arn:aws:s3:::mapbox-loading-dock/raw/mobile.codecoverage/*",
- "arn:aws:s3:::mapbox-loading-dock/raw/mobile_staging.docs_coverage/*",
- "arn:aws:s3:::mapbox-loading-dock/raw/mobile_staging.codecoverage/*",
- "arn:aws:s3:::mapbox-loading-dock/raw/mobile_staging.github_stats/*"
- ]
- }
- ]
- }
- },
- {
- "PolicyName": "list-loading-dock",
- "PolicyDocument": {
- "Statement": [
- {
- "Action": [
- "s3:ListBucket"
- ],
- "Resource": [
- "arn:aws:s3:::mapbox-loading-dock"
- ],
- "Effect": "Allow"
- }
- ]
- }
- },
- {
- "PolicyName": "cloudwatch-metrics",
- "PolicyDocument": {
- "Statement": [
- {
- "Action": [
- "cloudwatch:PutMetricData",
- "cloudwatch:GetMetricData",
- "cloudwatch:GetMetricStatistics"
- ],
- "Effect": "Allow",
- "Resource": [
- "*"
- ]
- }
- ]
- }
- },
- {
- "PolicyName": "publish-nightlies",
- "PolicyDocument": {
- "Statement": [
- {
- "Action": [
- "s3:DeleteObject",
- "s3:GetObject",
- "s3:GetObjectAcl",
- "s3:PutObject",
- "s3:PutObjectAcl"
- ],
- "Effect": "Allow",
- "Resource": [
- "arn:aws:s3:::mapbox/mapbox-gl-native/ios/builds/*"
- ]
- }
- ]
- }
- }
- ]
- }
- },
- "BuildUserKey": {
- "Type": "AWS::IAM::AccessKey",
- "Properties": {
- "UserName": {
- "Ref": "BuildUser"
- }
- }
- }
- },
- "Outputs": {
- "AccessKeyId": {
- "Value": {
- "Ref": "BuildUserKey"
- }
- },
- "SecretAccessKey": {
- "Value": {
- "Fn::GetAtt": [
- "BuildUserKey",
- "SecretAccessKey"
- ]
- }
- }
- }
-}
diff --git a/circle.yml b/circle.yml
index ceeca4bdd6..e69ef56e0d 100644
--- a/circle.yml
+++ b/circle.yml
@@ -15,8 +15,8 @@ workflows:
- android-arm-template:
name: android-gnustl-arm-v7
stl: gnustl_shared
- firebase_device_id: "htc_m8"
- firebase_device_os: "19"
+ firebase_device_id: "flo"
+ firebase_device_os: "21"
image: android-ndk-r17c:1d5db0eb34
abi: arm-v7
- android-release:
@@ -43,6 +43,7 @@ workflows:
name: linux-gcc4.9-debug
- linux-gcc5-debug-coverage
- linux-doxygen
+ - linux-render-tests
- ios-debug
- ios-release-template:
name: ios-release
@@ -53,6 +54,7 @@ workflows:
branches:
ignore: /.*/
- macos-debug
+ - macos-render-tests
- qt5-linux-gcc5-release
- qt5-macos-debug
nightly:
@@ -241,11 +243,25 @@ commands:
sed -i 's/"$@" 2>&1/"$@"/' /usr/bin/xvfb-run
+ configure-cmake:
+ steps:
+ - run:
+ name: CMake configuration step
+ command: |
+ mkdir -p build
+ cd build
+ cmake -DWITH_CXX11ABI=${WITH_CXX11ABI:0} -DWITH_COVERAGE=${WITH_COVERAGE:0} -DWITH_OSMESA=${WITH_OSMESA:0} -DWITH_EGL=${WITH_EGL:0} ..
+ cd ..
build-node:
steps:
- run:
name: Build node
command: make node-all
+ build-mbgl-render-test:
+ steps:
+ - run:
+ name: Build mbgl-render-test
+ command: cmake --build build --config ${BUILDTYPE} --target mbgl-render-test -- -j${JOBS}
build-linux:
steps:
- run:
@@ -371,18 +387,25 @@ commands:
xvfb-run --server-args="-screen 0 1024x768x24" \
logbt -- apitrace trace --api=egl -v make test-node
- run-node-linux-tests-recycle-map:
+ run-macos-render-tests:
+ steps:
+ - run:
+ name: Run render tests (mbgl-render-test)
+ command: |
+ build/mbgl-render-test --recycle-map --shuffle
+ no_output_timeout: 2m
+
+ run-linux-render-tests:
parameters:
node_version:
type: string
default: v8
steps:
- run:
- name: Run node tests (recycling the map object)
+ name: Run render tests (mbgl-render-test)
command: |
- . "$NVM_DIR/nvm.sh" && nvm use << parameters.node_version >>
xvfb-run --server-args="-screen 0 1024x768x24" \
- logbt -- apitrace trace --api=egl -v make test-node-recycle-map
+ logbt -- apitrace trace --api=egl -v build/mbgl-render-test --recycle-map --shuffle
run-unit-tests:
steps:
@@ -408,17 +431,11 @@ commands:
when: on_success
command: platform/node/scripts/publish.sh
-
upload-render-tests:
steps:
- store_artifacts:
path: mapbox-gl-js/test/integration/render-tests/index.html
destination: render-tests
- upload-render-tests-recycle-map:
- steps:
- - store_artifacts:
- path: mapbox-gl-js/test/integration/render-tests/index-recycle-map.html
- destination: render-tests
collect-xcode-build-logs:
steps:
@@ -513,7 +530,7 @@ jobs:
default: "c++_static"
image:
type: string
- default: android-ndk-r19:8e91a7ebab
+ default: android-ndk-r20:7b7c4b42cf
firebase_device_id:
type: string
default: sailfish
@@ -595,6 +612,8 @@ jobs:
path: platform/android/MapboxGLAndroidSDKTestApp/build/reports/lint-results.xml
- store_artifacts:
path: platform/android/MapboxGLAndroidSDKTestApp/lint-baseline.xml
+ - store_artifacts:
+ path: platform/android/MapboxGLAndroidSDK/build/intermediates/cmake/debug/obj
# ------------------------------------------------------------------------------
android-release:
@@ -726,7 +745,6 @@ jobs:
- save-dependencies
- run-node-linux-tests
- publish-node-package
- - upload-render-tests
# ------------------------------------------------------------------------------
node-gcc8-debug:
@@ -743,9 +761,7 @@ jobs:
- install-dependencies
- build-node
- save-dependencies
- - run-node-linux-tests-recycle-map
- publish-node-package
- - upload-render-tests-recycle-map
# ------------------------------------------------------------------------------
node-macos-release:
@@ -762,7 +778,6 @@ jobs:
- save-dependencies
- run-node-macos-tests
- publish-node-package
- - upload-render-tests
- collect-xcode-build-logs
- upload-xcode-build-logs
@@ -910,6 +925,25 @@ jobs:
scripts/publish_doxygen_coverage.js "build/linux-$(uname -m)/Debug/doxygen-coverage.json"
# ------------------------------------------------------------------------------
+ linux-render-tests:
+ docker:
+ - image: mbgl/linux-clang-7:a5a3c52107
+ resource_class: large
+ working_directory: /src
+ environment:
+ LIBSYSCONFCPUS: 4
+ JOBS: 4
+ BUILDTYPE: RelWithDebInfo
+ WITH_EGL: 1
+ steps:
+ - install-dependencies
+ - configure-cmake
+ - build-mbgl-render-test
+ - run-linux-render-tests
+ - save-dependencies
+ - upload-render-tests
+
+# ------------------------------------------------------------------------------
ios-debug:
macos:
xcode: "10.2.1"
@@ -925,7 +959,7 @@ jobs:
name: Check symbol namespacing for mapbox-events-ios
command: make ios-check-events-symbols
- run:
- name: Lint plist files
+ name: Lint podspecs and plist files
command: make ios-lint
- run:
name: Nitpick Darwin code generation
@@ -1119,6 +1153,23 @@ jobs:
- upload-xcode-build-logs
# ------------------------------------------------------------------------------
+ macos-render-tests:
+ macos:
+ xcode: "10.2.1"
+ environment:
+ BUILDTYPE: RelWithDebInfo
+ HOMEBREW_NO_AUTO_UPDATE: 1
+ JOBS: 2
+ steps:
+ - install-macos-dependencies
+ - install-dependencies
+ - configure-cmake
+ - build-mbgl-render-test
+ - save-dependencies
+ - run-macos-render-tests
+ - upload-render-tests
+
+# ------------------------------------------------------------------------------
qt5-linux-gcc5-release:
docker:
- image: mbgl/linux-gcc-5-qt-5.9:5132cfd29f
diff --git a/cloudformation/ci.template.js b/cloudformation/ci.template.js
new file mode 100644
index 0000000000..e0e1fc0603
--- /dev/null
+++ b/cloudformation/ci.template.js
@@ -0,0 +1,117 @@
+'use strict';
+
+const template = {
+ AWSTemplateFormatVersion: '2010-09-09',
+ Description: 'mapbox-gl-native travis resources',
+ Resources: {
+ BuildUser: {
+ Type: 'AWS::IAM::User',
+ Properties: {
+ Policies: [
+ {
+ PolicyName: 'listBuckets',
+ PolicyDocument: {
+ Statement: [
+ {
+ Action: [ 's3:ListBucket' ],
+ Effect: 'Allow',
+ Resource: [ 'arn:aws:s3:::mapbox' ],
+ Condition: { StringLike: { 's3:prefix': 'mapbox-gl-native/*' } }
+ },
+ {
+ Action: [ 's3:ListBucket' ],
+ Resource: [ 'arn:aws:s3:::mapbox-node-binary' ],
+ Effect: 'Allow'
+ },
+ {
+ Action: [ 's3:ListBucket' ],
+ Resource: [ 'arn:aws:s3:::mapbox-loading-dock' ],
+ Effect: 'Allow'
+ }
+ ]
+ }
+ },
+ {
+ PolicyName: 'build-testing',
+ PolicyDocument: {
+ Statement: [
+ {
+ Action: [
+ 's3:GetObject',
+ 's3:GetObjectAcl',
+ 's3:PutObject',
+ 's3:PutObjectAcl'
+ ],
+ Effect: 'Allow',
+ Resource: [
+ 'arn:aws:s3:::mapbox/mapbox-gl-native/*',
+ 'arn:aws:s3:::mapbox-node-binary/@mapbox/mapbox-gl-native/*',
+ 'arn:aws:s3:::mapbox/mapbox-gl-native/ios/builds/*'
+ ]
+ }
+ ]
+ }
+ },
+ {
+ PolicyName: 'cloudwatch-metrics',
+ PolicyDocument: {
+ Statement: [
+ {
+ Action: [
+ 'cloudwatch:PutMetricData',
+ 'cloudwatch:GetMetricData',
+ 'cloudwatch:GetMetricStatistics'
+ ],
+ Effect: 'Allow',
+ Resource: [ '*' ]
+ }
+ ]
+ }
+ },
+ {
+ PolicyName: 'get-signing-key',
+ PolicyDocument: {
+ Statement: [
+ {
+ Action: [ 's3:GetObject' ],
+ Effect: 'Allow',
+ Resource: [
+ 'arn:aws:s3:::mapbox/android/signing-credentials/secring.gpg'
+ ]
+ }
+ ]
+ }
+ },
+ {
+ PolicyName: 'publish-metrics',
+ PolicyDocument: {
+ Statement: [
+ {
+ Action: [ 's3:PutObject', 's3:GetObject', 's3:GetObjectAcl' ],
+ Effect: 'Allow',
+ Resource: [
+ 'arn:aws:s3:::mapbox-loading-dock/raw/mobile.binarysize/*',
+ 'arn:aws:s3:::mapbox-loading-dock/raw/mobile.codecoverage/*',
+ 'arn:aws:s3:::mapbox-loading-dock/raw/mobile_staging.docs_coverage/*',
+ 'arn:aws:s3:::mapbox-loading-dock/raw/mobile_staging.codecoverage/*',
+ 'arn:aws:s3:::mapbox-loading-dock/raw/mobile_staging.github_stats/*'
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ BuildUserKey: {
+ Type: 'AWS::IAM::AccessKey',
+ Properties: { UserName: { Ref: 'BuildUser' } }
+ }
+ },
+ Outputs: {
+ AccessKeyId: { Value: { Ref: 'BuildUserKey' } },
+ SecretAccessKey: { Value: { 'Fn::GetAtt': [ 'BuildUserKey', 'SecretAccessKey' ] } }
+ }
+};
+
+module.exports = template;
diff --git a/cmake/core.cmake b/cmake/core.cmake
index 1971352c37..3278c4f7d3 100644
--- a/cmake/core.cmake
+++ b/cmake/core.cmake
@@ -19,13 +19,18 @@ target_link_libraries(mbgl-core PRIVATE
wagyu
)
+# linux uses ICU from mason, other platforms use vendored ICU
+if(NOT MBGL_PLATFORM STREQUAL "linux")
+ set(ICU_LIBRARY "icu")
+endif()
+
# FIXME: We should not leak these many
# libraries in our public interface.
target_link_libraries(mbgl-core PUBLIC
boost
geojson.hpp
geometry.hpp
- icu
+ ${ICU_LIBRARY}
optional
polylabel
protozero
diff --git a/cmake/mason-dependencies.cmake b/cmake/mason-dependencies.cmake
index a6ef7a3a53..5ec6a44a79 100644
--- a/cmake/mason-dependencies.cmake
+++ b/cmake/mason-dependencies.cmake
@@ -8,6 +8,7 @@ elseif(MBGL_PLATFORM STREQUAL "linux")
mason_use(libuv VERSION 1.9.1)
mason_use(libpng VERSION 1.6.25)
mason_use(libjpeg-turbo VERSION 1.5.0)
+ mason_use(icu VERSION 63.1-min-static-data)
if(WITH_EGL)
mason_use(swiftshader VERSION 2018-05-31)
diff --git a/cmake/mbgl.cmake b/cmake/mbgl.cmake
index ef948e6caf..1b4338a0bb 100644
--- a/cmake/mbgl.cmake
+++ b/cmake/mbgl.cmake
@@ -219,6 +219,9 @@ function(initialize_xcode_cxx_build_settings target)
# Make all build configurations debuggable — except Release.
set_xcode_property(${target} GCC_GENERATE_DEBUGGING_SYMBOLS $<$<NOT:$<CONFIG:Release>>:YES>)
+ # -Wunguarded-availability
+ set_xcode_property(${target} CLANG_WARN_UNGUARDED_AVAILABILITY YES_AGGRESSIVE)
+
if (DEFINED ENV{CI})
set_xcode_property(${target} COMPILER_INDEX_STORE_ENABLE NO)
endif()
diff --git a/cmake/render-test.cmake b/cmake/render-test.cmake
new file mode 100644
index 0000000000..6505cc73f8
--- /dev/null
+++ b/cmake/render-test.cmake
@@ -0,0 +1,29 @@
+add_executable(mbgl-render-test
+ render-test/main.cpp
+ render-test/parser.cpp
+ render-test/runner.cpp
+)
+
+if(APPLE)
+ target_link_libraries(mbgl-render-test PRIVATE mbgl-loop-darwin)
+else()
+ target_link_libraries(mbgl-render-test PRIVATE mbgl-loop-uv)
+endif()
+
+target_include_directories(mbgl-render-test
+ PRIVATE src
+ PRIVATE platform/default/include
+ PRIVATE render-test
+)
+
+target_link_libraries(mbgl-render-test PRIVATE
+ mbgl-core
+ mbgl-filesource
+ args
+ expected
+ filesystem
+ pixelmatch-cpp
+ rapidjson
+)
+
+add_definitions(-DTEST_RUNNER_ROOT_PATH="${CMAKE_SOURCE_DIR}")
diff --git a/cmake/sqlite.cmake b/cmake/sqlite.cmake
index fd2d98d3e2..cc579b22ac 100644
--- a/cmake/sqlite.cmake
+++ b/cmake/sqlite.cmake
@@ -34,5 +34,14 @@ target_compile_definitions(sqlite
target_compile_options(sqlite
PRIVATE "-Wno-int-conversion"
PRIVATE "-Wno-implicit-fallthrough"
- PRIVATE "-Wno-cast-function-type"
)
+
+if(CMAKE_COMPILER_IS_GNUCXX)
+ if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 8.0)
+ target_compile_options(sqlite PRIVATE "-Wno-cast-function-type")
+ else()
+ target_compile_options(sqlite PRIVATE "-Wno-bad-function-cast")
+ endif()
+elseif(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang")
+ target_compile_options(sqlite PRIVATE "-Wno-cast-function-type")
+endif()
diff --git a/cmake/vendor.cmake b/cmake/vendor.cmake
index 0b2deeb552..bc1bad757f 100644
--- a/cmake/vendor.cmake
+++ b/cmake/vendor.cmake
@@ -9,6 +9,7 @@ include(${CMAKE_SOURCE_DIR}/vendor/cheap-ruler-cpp.cmake)
include(${CMAKE_SOURCE_DIR}/vendor/earcut.hpp.cmake)
include(${CMAKE_SOURCE_DIR}/vendor/eternal.cmake)
include(${CMAKE_SOURCE_DIR}/vendor/expected.cmake)
+include(${CMAKE_SOURCE_DIR}/vendor/filesystem.cmake)
include(${CMAKE_SOURCE_DIR}/vendor/geojson-vt-cpp.cmake)
include(${CMAKE_SOURCE_DIR}/vendor/geojson.hpp.cmake)
include(${CMAKE_SOURCE_DIR}/vendor/geometry.hpp.cmake)
diff --git a/include/mbgl/storage/default_file_source.hpp b/include/mbgl/storage/default_file_source.hpp
index 130f4f1022..4f4c7136c6 100644
--- a/include/mbgl/storage/default_file_source.hpp
+++ b/include/mbgl/storage/default_file_source.hpp
@@ -23,24 +23,11 @@ using PathChangeCallback = std::function<void ()>;
class DefaultFileSource : public FileSource {
public:
- /*
- * The maximumCacheSize parameter is a limit applied to non-offline resources only,
- * i.e. resources added to the database for the "ambient use" caching functionality.
- * There is no size limit for offline resources. If a user never creates any offline
- * regions, we want the database to remain fairly small (order tens or low hundreds
- * of megabytes).
- */
- DefaultFileSource(const std::string& cachePath,
- const std::string& assetPath,
- uint64_t maximumCacheSize = util::DEFAULT_MAX_CACHE_SIZE);
- DefaultFileSource(const std::string& cachePath,
- std::unique_ptr<FileSource>&& assetFileSource,
- uint64_t maximumCacheSize = util::DEFAULT_MAX_CACHE_SIZE);
+ DefaultFileSource(const std::string& cachePath, const std::string& assetPath, bool supportCacheOnlyRequests = true);
+ DefaultFileSource(const std::string& cachePath, std::unique_ptr<FileSource>&& assetFileSource, bool supportCacheOnlyRequests = true);
~DefaultFileSource() override;
- bool supportsCacheOnlyRequests() const override {
- return true;
- }
+ bool supportsCacheOnlyRequests() const override;
void setAPIBaseURL(const std::string&);
std::string getAPIBaseURL();
@@ -192,27 +179,55 @@ public:
* executed on the database thread; it is the responsibility of the SDK bindings
* to re-execute a user-provided callback on the main thread.
*/
- void resetCache(std::function<void (std::exception_ptr)>);
+ void resetDatabase(std::function<void (std::exception_ptr)>);
/*
- * Forces revalidation of tiles in the ambient cache.
+ * Forces revalidation of the ambient cache.
*
- * Forces Mapbox GL Native to revalidate tiles stored in the ambient
+ * Forces Mapbox GL Native to revalidate resources stored in the ambient
* cache with the tile server before using them, making sure they
* are the latest version. This is more efficient than cleaning the
- * cache because if the tile is considered valid after the server
+ * cache because if the resource is considered valid after the server
* lookup, it will not get downloaded again.
+ *
+ * Resources overlapping with offline regions will not be affected
+ * by this call.
*/
- void invalidateTileCache(std::function<void (std::exception_ptr)>);
+ void invalidateAmbientCache(std::function<void (std::exception_ptr)>);
/*
- * Erase tiles from the ambient cache, freeing storage space.
+ * Erase resources from the ambient cache, freeing storage space.
*
- * Erases the tile cache, freeing resources. This operation can be
+ * Erases the ambient cache, freeing resources. This operation can be
* potentially slow because it will trigger a VACUUM on SQLite,
* forcing the database to move pages on the filesystem.
+ *
+ * Resources overlapping with offline regions will not be affected
+ * by this call.
*/
- void clearTileCache(std::function<void (std::exception_ptr)>);
+ void clearAmbientCache(std::function<void (std::exception_ptr)>);
+
+ /*
+ * Sets the maximum size in bytes for the ambient cache.
+ *
+ * This call is potentially expensive because it will try
+ * to trim the data in case the database is larger than the
+ * size defined. The size of offline regions are not affected
+ * by this settings, but the ambient cache will always try
+ * to not exceed the maximum size defined, taking into account
+ * the current size for the offline regions.
+ *
+ * If the maximum size is set to 50 MB and 40 MB are already
+ * used by offline regions, the cache size will be effectively
+ * 10 MB.
+ *
+ * Setting the size to 0 will disable the cache if there is no
+ * offline region on the database.
+ *
+ * This method should always be called before using the database,
+ * otherwise the default maximum size will be used.
+ */
+ void setMaximumAmbientCacheSize(uint64_t size, std::function<void (std::exception_ptr)> callback);
// For testing only.
void setOnlineStatus(bool);
@@ -229,6 +244,8 @@ private:
std::mutex cachedAccessTokenMutex;
std::string cachedAccessToken;
+
+ const bool supportCacheOnlyRequests;
};
} // namespace mbgl
diff --git a/include/mbgl/storage/resource_options.hpp b/include/mbgl/storage/resource_options.hpp
index 6d603b8cca..00dc6e10df 100644
--- a/include/mbgl/storage/resource_options.hpp
+++ b/include/mbgl/storage/resource_options.hpp
@@ -97,6 +97,21 @@ public:
uint64_t maximumCacheSize() const;
/**
+ * @brief Sets whether to support cache-only requests.
+ *
+ * @return Whether or not cache-only requests are supported.
+ */
+ bool supportsCacheOnlyRequests() const;
+
+ /**
+ * @brief Gets the previously set (or default) support for cache-only requests.
+ *
+ * @param cacheOnly Whether or not cache-only requests are supported.
+ * @return reference to ResourceOptions for chaining options together.
+ */
+ ResourceOptions& withCacheOnlyRequestsSupport(bool cacheOnly);
+
+ /**
* @brief Sets the platform context. A platform context is usually an object
* that assists the creation of a file source.
*
diff --git a/include/mbgl/style/expression/expression.hpp b/include/mbgl/style/expression/expression.hpp
index 22ae57c2be..5f66fc6dc7 100644
--- a/include/mbgl/style/expression/expression.hpp
+++ b/include/mbgl/style/expression/expression.hpp
@@ -142,7 +142,8 @@ enum class Kind : int32_t {
All,
Comparison,
FormatExpression,
- FormatSectionOverride
+ FormatSectionOverride,
+ NumberFormat
};
class Expression {
diff --git a/include/mbgl/style/expression/number_format.hpp b/include/mbgl/style/expression/number_format.hpp
new file mode 100644
index 0000000000..9571c7d98a
--- /dev/null
+++ b/include/mbgl/style/expression/number_format.hpp
@@ -0,0 +1,40 @@
+#pragma once
+
+#include <mbgl/style/expression/expression.hpp>
+#include <mbgl/style/expression/parsing_context.hpp>
+
+namespace mbgl {
+namespace style {
+namespace expression {
+
+class NumberFormat final : public Expression {
+public:
+ NumberFormat(std::unique_ptr<Expression> number_,
+ std::unique_ptr<Expression> locale_,
+ std::unique_ptr<Expression> currency_,
+ std::unique_ptr<Expression> minFractionDigits_,
+ std::unique_ptr<Expression> maxFractionDigits_);
+
+ ~NumberFormat();
+
+ static ParseResult parse(const mbgl::style::conversion::Convertible& value, ParsingContext& ctx);
+
+ EvaluationResult evaluate(const EvaluationContext& params) const override;
+ void eachChild(const std::function<void(const Expression&)>& visit) const override;
+ bool operator==(const Expression& e) const override;
+ std::vector<optional<Value>> possibleOutputs() const override;
+
+ mbgl::Value serialize() const override;
+ std::string getOperator() const override { return "number-format"; }
+
+private:
+ std::unique_ptr<Expression> number;
+ std::unique_ptr<Expression> locale;
+ std::unique_ptr<Expression> currency;
+ std::unique_ptr<Expression> minFractionDigits;
+ std::unique_ptr<Expression> maxFractionDigits;
+};
+
+} // namespace expression
+} // namespace style
+} // namespace mbgl
diff --git a/include/mbgl/style/layer_properties.hpp b/include/mbgl/style/layer_properties.hpp
index 301a048e50..c238faf02f 100644
--- a/include/mbgl/style/layer_properties.hpp
+++ b/include/mbgl/style/layer_properties.hpp
@@ -13,7 +13,8 @@ namespace style {
class LayerProperties {
public:
virtual ~LayerProperties() = default;
-
+ // Returns constants mask for the data-driven properties.
+ virtual unsigned long constantsMask() const { return 0u; }
Immutable<Layer::Impl> baseImpl;
protected:
diff --git a/include/mbgl/util/platform.hpp b/include/mbgl/util/platform.hpp
index 3544659740..2e11e5f186 100644
--- a/include/mbgl/util/platform.hpp
+++ b/include/mbgl/util/platform.hpp
@@ -16,6 +16,9 @@ std::string lowercase(const std::string &string);
// Gets the name of the current thread.
std::string getCurrentThreadName();
+std::string formatNumber(double number, const std::string& localeId, const std::string& currency,
+ uint8_t minFractionDigits, uint8_t maxFractionDigits);
+
// Set the name of the current thread, truncated at 15.
void setCurrentThreadName(const std::string& name);
diff --git a/package.json b/package.json
index 7b12cdcb79..991b115446 100644
--- a/package.json
+++ b/package.json
@@ -45,7 +45,6 @@
"install": "node-pre-gyp install --fallback-to-build=false || make node",
"test": "tape platform/node/test/js/**/*.test.js",
"test-memory": "node --expose-gc platform/node/test/memory.test.js",
- "test-suite": "run-s test-render test-query test-expressions",
"test-expressions": "node -r esm platform/node/test/expression.test.js",
"test-render": "node -r esm platform/node/test/render.test.js",
"test-query": "node -r esm platform/node/test/query.test.js"
diff --git a/platform/android/CHANGELOG.md b/platform/android/CHANGELOG.md
index 968dd00bfe..9288256692 100644
--- a/platform/android/CHANGELOG.md
+++ b/platform/android/CHANGELOG.md
@@ -4,13 +4,46 @@ Mapbox welcomes participation and contributions from everyone. If you'd like to
## master
+## 8.2.0-alpha.2 - July 3, 2019
+
### Bugs
- - Fixed queryRenderedFeatues bug caused by incorrect sort feature index calculation [#14884](https://github.com/mapbox/mapbox-gl-native/pull/14884)
+ - Fixed style change transition regression caused by delayed setting of the updated layer properties [#15016](https://github.com/mapbox/mapbox-gl-native/pull/15016)
+
+### Features
+ - Cache management API [#14962](https://github.com/mapbox/mapbox-gl-native/pull/14962)
+ - Include Hiragana and Katakana glyph ranges to LocalGlyphRasterizer [#15009](https://github.com/mapbox/mapbox-gl-native/pull/15009)
+
+### Build
+ - Update CI image with NDK r20 [#15005](https://github.com/mapbox/mapbox-gl-native/pull/15005)
-### Styles and rendering
+## 8.2.0-alpha.1 - June 26, 2019
+### Bugs
+ - Fixed queryRenderedFeatues bug caused by incorrect sort feature index calculation [#14884](https://github.com/mapbox/mapbox-gl-native/pull/14884)
+ - Switch back to a more compact line attributes layout [#14851](https://github.com/mapbox/mapbox-gl-native/pull/14851)
+ - Correct reset viewport restriction [#14882](https://github.com/mapbox/mapbox-gl-native/pull/14882)
+ - Allow null updates to GeoJsonSource [#14898](https://github.com/mapbox/mapbox-gl-native/pull/14898)
+ - Accept floating point numbers during core and platform color conversion [#14954](https://github.com/mapbox/mapbox-gl-native/pull/14954)
- Implemented asymmetric center of perspective: fixed an issue that caused the focal point to be always based on the view's horizontal center when setting [MapboxMap setPadding](https://docs.mapbox.com/android/api/map-sdk/8.0.0/com/mapbox/mapboxsdk/maps/MapboxMap.html#setPadding-int-int-int-int-). [#14664](https://github.com/mapbox/mapbox-gl-native/pull/14664)
+### Features
+ - Add resource usage to HttpRequest, add offline query flag to HttpRequest [#14837](https://github.com/mapbox/mapbox-gl-native/pull/14837)
+
+## 8.1.0 - June 20, 2019
+
+### Bugs
+ - Accept floating numbers during core->platform color conversion [#14954](https://github.com/mapbox/mapbox-gl-native/pull/14954)
+ - Ensure to re-enable move when quickzoom finishes [#14965](https://github.com/mapbox/mapbox-gl-native/pull/14965)
+
+### Features
+ - Limit concurrent request for Android 4.4 and below to 10 [#14971](https://github.com/mapbox/mapbox-gl-native/pull/14971)
+
+_Note about #14971:_ This pr makes an adjustment as a precautionary measure towards a rare bug that was discovered via instrumented unit tests on Android 4.4 (aka KitKat). This bug isn’t tied to a code change in the Mapbox Maps SDK for Android. Rather, the root cause is on the Android OS (and how it interacts with OkHttp).
+
+The Maps SDK doesn’t directly communicate with this system library that the bug originates from, but goes through an external dependency that handles the http communication for the Maps SDK. Capturing from the upstream repository that the code path to this system library isn’t thread safe in Android 4.4 and can result in the native crash above. The only way to fix this issue is by upgrading the end-user device to Android 5.0 or higher which isn’t possible if the device manufacturer hasn’t released an update for it.
+
+Please see https://github.com/square/okhttp/issues/2679 for more information.
+
## 8.1.0-beta.1 - June 13, 2019
### Bugs
@@ -18,7 +51,6 @@ Mapbox welcomes participation and contributions from everyone. If you'd like to
- Fix reset behavior for setLatLngBoundsForCameraTarget [#14882](https://github.com/mapbox/mapbox-gl-native/pull/14882)
### Features
- - Change library loader to soloader [#14890](https://github.com/mapbox/mapbox-gl-native/pull/14890)
- Add resource usage to HttpRequest, add offline query param [#14837](https://github.com/mapbox/mapbox-gl-native/pull/14837)
## 8.0.1 - June 11, 2019
diff --git a/platform/android/MapboxGLAndroidSDK/gradle.properties b/platform/android/MapboxGLAndroidSDK/gradle.properties
index 9278f92721..d996dd7eca 100644
--- a/platform/android/MapboxGLAndroidSDK/gradle.properties
+++ b/platform/android/MapboxGLAndroidSDK/gradle.properties
@@ -1,4 +1,4 @@
-VERSION_NAME=8.1.0-SNAPSHOT
+VERSION_NAME=8.2.0-SNAPSHOT
# Only build native dependencies for the current ABI
# See https://code.google.com/p/android/issues/detail?id=221098#c20
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/camera/CameraUpdateFactory.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/camera/CameraUpdateFactory.java
index c4cc6d0196..5ff4104ea8 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/camera/CameraUpdateFactory.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/camera/CameraUpdateFactory.java
@@ -4,13 +4,13 @@ import android.graphics.Point;
import android.graphics.PointF;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
-
import android.support.annotation.Nullable;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.geometry.LatLngBounds;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.maps.Projection;
import com.mapbox.mapboxsdk.maps.UiSettings;
+import junit.framework.Assert;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -45,9 +45,11 @@ public final class CameraUpdateFactory {
/**
* Returns a CameraUpdate that transforms the camera such that the specified
- * latitude/longitude bounds are centered on screen at the greatest possible zoom level.
+ * latitude/longitude bounds are centered on screen at the greatest possible zoom level while maintaining
+ * current camera position bearing and tilt values.
+ * <p>
* You can specify padding, in order to inset the bounding box from the map view's edges.
- * The returned CameraUpdate has a bearing of 0 and a tilt of 0.
+ * </p>
*
* @param bounds Bounds to match Camera position with
* @param padding Padding added to the bounds
@@ -59,9 +61,29 @@ public final class CameraUpdateFactory {
/**
* Returns a CameraUpdate that transforms the camera such that the specified
- * latitude/longitude bounds are centered on screen at the greatest possible zoom level.
+ * latitude/longitude bounds are centered on screen at the greatest possible zoom level while using
+ * provided bearing and tilt values.
+ * <p>
+ * You can specify padding, in order to inset the bounding box from the map view's edges.
+ * </p>
+ *
+ * @param bounds Bounds to match Camera position with
+ * @param bearing Bearing to take in account when generating the bounds
+ * @param tilt Tilt to take in account when generating the bounds
+ * @param padding Padding added to the bounds
+ * @return CameraUpdate Final Camera Position
+ */
+ public static CameraUpdate newLatLngBounds(@NonNull LatLngBounds bounds, double bearing, double tilt, int padding) {
+ return newLatLngBounds(bounds, bearing, tilt, padding, padding, padding, padding);
+ }
+
+ /**
+ * Returns a CameraUpdate that transforms the camera such that the specified
+ * latitude/longitude bounds are centered on screen at the greatest possible zoom level while maintaining
+ * current camera position bearing and tilt values.
+ * <p>
* You can specify padding, in order to inset the bounding box from the map view's edges.
- * The returned CameraUpdate has a bearing of 0 and a tilt of 0.
+ * </p>
*
* @param bounds Bounds to base the Camera position out of
* @param paddingLeft Padding left of the bounds
@@ -72,7 +94,29 @@ public final class CameraUpdateFactory {
*/
public static CameraUpdate newLatLngBounds(@NonNull LatLngBounds bounds, int paddingLeft, int paddingTop,
int paddingRight, int paddingBottom) {
- return new CameraBoundsUpdate(bounds, paddingLeft, paddingTop, paddingRight, paddingBottom);
+ return new CameraBoundsUpdate(bounds, null, null, paddingLeft, paddingTop, paddingRight, paddingBottom);
+ }
+
+ /**
+ * Returns a CameraUpdate that transforms the camera such that the specified
+ * latitude/longitude bounds are centered on screen at the greatest possible zoom level while using
+ * provided bearing and tilt values.
+ * <p>
+ * You can specify padding, in order to inset the bounding box from the map view's edges.
+ * </p>
+ *
+ * @param bounds Bounds to base the Camera position out of
+ * @param bearing Bearing to take in account when generating the bounds
+ * @param tilt Tilt to take in account when generating the bounds
+ * @param paddingLeft Padding left of the bounds
+ * @param paddingTop Padding top of the bounds
+ * @param paddingRight Padding right of the bounds
+ * @param paddingBottom Padding bottom of the bounds
+ * @return CameraUpdate Final Camera Position
+ */
+ public static CameraUpdate newLatLngBounds(@NonNull LatLngBounds bounds, double bearing, double tilt,
+ int paddingLeft, int paddingTop, int paddingRight, int paddingBottom) {
+ return new CameraBoundsUpdate(bounds, bearing, tilt, paddingLeft, paddingTop, paddingRight, paddingBottom);
}
/**
@@ -253,16 +297,21 @@ public final class CameraUpdateFactory {
static final class CameraBoundsUpdate implements CameraUpdate {
- private LatLngBounds bounds;
- private int[] padding;
+ private final LatLngBounds bounds;
+ private final int[] padding;
+ private final Double bearing;
+ private final Double tilt;
- CameraBoundsUpdate(LatLngBounds bounds, int[] padding) {
+ CameraBoundsUpdate(LatLngBounds bounds, Double bearing, Double tilt, int[] padding) {
this.bounds = bounds;
this.padding = padding;
+ this.bearing = bearing;
+ this.tilt = tilt;
}
- CameraBoundsUpdate(LatLngBounds bounds, int paddingLeft, int paddingTop, int paddingRight, int paddingBottom) {
- this(bounds, new int[] {paddingLeft, paddingTop, paddingRight, paddingBottom});
+ CameraBoundsUpdate(LatLngBounds bounds, Double bearing, Double tilt, int paddingLeft,
+ int paddingTop, int paddingRight, int paddingBottom) {
+ this(bounds, bearing, tilt, new int[] {paddingLeft, paddingTop, paddingRight, paddingBottom});
}
public LatLngBounds getBounds() {
@@ -275,7 +324,15 @@ public final class CameraUpdateFactory {
@Override
public CameraPosition getCameraPosition(@NonNull MapboxMap mapboxMap) {
- return mapboxMap.getCameraForLatLngBounds(bounds, padding);
+ if (bearing == null && tilt == null) {
+ // use current camera position tilt and bearing
+ return mapboxMap.getCameraForLatLngBounds(bounds, padding);
+ } else {
+ // use provided tilt and bearing
+ Assert.assertNotNull(bearing);
+ Assert.assertNotNull(tilt);
+ return mapboxMap.getCameraForLatLngBounds(bounds, padding, bearing, tilt);
+ }
}
@Override
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java
index 50bbb7acfc..6337287770 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java
@@ -347,6 +347,34 @@ final class LocationAnimatorCoordinator {
createNewFloatAnimator(ANIMATOR_CAMERA_COMPASS_BEARING, previousCameraBearing, normalizedCameraBearing);
}
+ void resetAllLayerAnimations() {
+ MapboxLatLngAnimator latLngAnimator = (MapboxLatLngAnimator) animatorArray.get(ANIMATOR_LAYER_LATLNG);
+ MapboxFloatAnimator gpsBearingAnimator = (MapboxFloatAnimator) animatorArray.get(ANIMATOR_LAYER_GPS_BEARING);
+ MapboxFloatAnimator compassBearingAnimator =
+ (MapboxFloatAnimator) animatorArray.get(ANIMATOR_LAYER_COMPASS_BEARING);
+
+ if (latLngAnimator != null && gpsBearingAnimator != null) {
+ LatLng currentLatLng = (LatLng) latLngAnimator.getAnimatedValue();
+ LatLng currentLatLngTarget = latLngAnimator.getTarget();
+ createNewLatLngAnimator(ANIMATOR_LAYER_LATLNG, currentLatLng, currentLatLngTarget);
+
+ float currentGpsBearing = (float) gpsBearingAnimator.getAnimatedValue();
+ float currentGpsBearingTarget = gpsBearingAnimator.getTarget();
+ createNewFloatAnimator(ANIMATOR_LAYER_GPS_BEARING, currentGpsBearing, currentGpsBearingTarget);
+
+ playAnimators(getAnimationDuration(), ANIMATOR_LAYER_LATLNG, ANIMATOR_LAYER_GPS_BEARING);
+ }
+
+ if (compassBearingAnimator != null) {
+ float currentLayerBearing = getPreviousLayerCompassBearing();
+ float currentLayerBearingTarget = compassBearingAnimator.getTarget();
+ createNewFloatAnimator(ANIMATOR_LAYER_COMPASS_BEARING, currentLayerBearing, currentLayerBearingTarget);
+ playAnimators(
+ compassAnimationEnabled ? COMPASS_UPDATE_RATE_MS : 0,
+ ANIMATOR_LAYER_COMPASS_BEARING);
+ }
+ }
+
void cancelZoomAnimation() {
cancelAnimator(ANIMATOR_ZOOM);
}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java
index 5d8847eab4..5b2dcd8554 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java
@@ -1393,6 +1393,9 @@ public final class LocationComponent {
animationsValueChangeListeners.addAll(locationLayerController.getAnimationListeners());
animationsValueChangeListeners.addAll(locationCameraController.getAnimationListeners());
locationAnimatorCoordinator.updateAnimatorListenerHolders(animationsValueChangeListeners);
+ locationAnimatorCoordinator.resetAllCameraAnimations(mapboxMap.getCameraPosition(),
+ locationCameraController.getCameraMode() == CameraMode.TRACKING_GPS_NORTH);
+ locationAnimatorCoordinator.resetAllLayerAnimations();
}
@NonNull
@@ -1517,7 +1520,8 @@ public final class LocationComponent {
}
@NonNull
- private OnCameraTrackingChangedListener cameraTrackingChangedListener = new OnCameraTrackingChangedListener() {
+ @VisibleForTesting
+ OnCameraTrackingChangedListener cameraTrackingChangedListener = new OnCameraTrackingChangedListener() {
@Override
public void onCameraTrackingDismissed() {
for (OnCameraTrackingChangedListener listener : onCameraTrackingChangedListeners) {
@@ -1530,8 +1534,6 @@ public final class LocationComponent {
locationAnimatorCoordinator.cancelZoomAnimation();
locationAnimatorCoordinator.cancelTiltAnimation();
updateAnimatorListenerHolders();
- locationAnimatorCoordinator.resetAllCameraAnimations(mapboxMap.getCameraPosition(),
- locationCameraController.getCameraMode() == CameraMode.TRACKING_GPS_NORTH);
for (OnCameraTrackingChangedListener listener : onCameraTrackingChangedListeners) {
listener.onCameraTrackingChanged(currentMode);
}
@@ -1539,7 +1541,8 @@ public final class LocationComponent {
};
@NonNull
- private OnRenderModeChangedListener renderModeChangedListener = new OnRenderModeChangedListener() {
+ @VisibleForTesting
+ OnRenderModeChangedListener renderModeChangedListener = new OnRenderModeChangedListener() {
@Override
public void onRenderModeChanged(int currentMode) {
updateAnimatorListenerHolders();
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/log/Logger.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/log/Logger.java
index 4a262a8529..b6c4bc8722 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/log/Logger.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/log/Logger.java
@@ -1,8 +1,13 @@
package com.mapbox.mapboxsdk.log;
+import android.support.annotation.IntDef;
import android.support.annotation.Keep;
import android.util.Log;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+
/**
* Logger for the Mapbox Maps SDK for Android
* <p>
@@ -68,6 +73,22 @@ public final class Logger {
private static volatile LoggerDefinition logger = DEFAULT;
+ @LogLevel
+ private static int logLevel;
+
+ /**
+ * Set the verbosity of the Logger.
+ * <p>
+ * This configuration can be used to have more granular control over which logs are emitted by the
+ * Mapbox Maps SDK for Android.
+ * </p>
+ *
+ * @param logLevel the verbosity level
+ */
+ public static void setVerbosity(@LogLevel int logLevel) {
+ Logger.logLevel = logLevel;
+ }
+
/**
* Replace the current used logger definition.
*
@@ -85,7 +106,9 @@ public final class Logger {
* @param msg The message you would like logged.
*/
public static void v(String tag, String msg) {
- logger.v(tag, msg);
+ if (logLevel <= VERBOSE) {
+ logger.v(tag, msg);
+ }
}
/**
@@ -97,7 +120,9 @@ public final class Logger {
* @param tr An exception to log
*/
public static void v(String tag, String msg, Throwable tr) {
- logger.v(tag, msg, tr);
+ if (logLevel <= VERBOSE) {
+ logger.v(tag, msg, tr);
+ }
}
/**
@@ -108,7 +133,9 @@ public final class Logger {
* @param msg The message you would like logged.
*/
public static void d(String tag, String msg) {
- logger.d(tag, msg);
+ if (logLevel <= DEBUG) {
+ logger.d(tag, msg);
+ }
}
/**
@@ -120,7 +147,9 @@ public final class Logger {
* @param tr An exception to log
*/
public static void d(String tag, String msg, Throwable tr) {
- logger.d(tag, msg, tr);
+ if (logLevel <= DEBUG) {
+ logger.d(tag, msg, tr);
+ }
}
/**
@@ -131,7 +160,9 @@ public final class Logger {
* @param msg The message you would like logged.
*/
public static void i(String tag, String msg) {
- logger.i(tag, msg);
+ if (logLevel <= INFO) {
+ logger.i(tag, msg);
+ }
}
/**
@@ -143,7 +174,9 @@ public final class Logger {
* @param tr An exception to log
*/
public static void i(String tag, String msg, Throwable tr) {
- logger.i(tag, msg, tr);
+ if (logLevel <= INFO) {
+ logger.i(tag, msg, tr);
+ }
}
/**
@@ -154,7 +187,9 @@ public final class Logger {
* @param msg The message you would like logged.
*/
public static void w(String tag, String msg) {
- logger.w(tag, msg);
+ if (logLevel <= WARN) {
+ logger.w(tag, msg);
+ }
}
/**
@@ -166,7 +201,9 @@ public final class Logger {
* @param tr An exception to log
*/
public static void w(String tag, String msg, Throwable tr) {
- logger.w(tag, msg, tr);
+ if (logLevel <= WARN) {
+ logger.w(tag, msg, tr);
+ }
}
/**
@@ -177,7 +214,9 @@ public final class Logger {
* @param msg The message you would like logged.
*/
public static void e(String tag, String msg) {
- logger.e(tag, msg);
+ if (logLevel <= ERROR) {
+ logger.e(tag, msg);
+ }
}
/**
@@ -189,7 +228,9 @@ public final class Logger {
* @param tr An exception to log
*/
public static void e(String tag, String msg, Throwable tr) {
- logger.e(tag, msg, tr);
+ if (logLevel <= ERROR) {
+ logger.e(tag, msg, tr);
+ }
}
/**
@@ -221,4 +262,60 @@ public final class Logger {
throw new UnsupportedOperationException();
}
}
+
+ /**
+ * Priority constant for the println method; use Logger.v
+ * <p>
+ * This log level will print all logs.
+ * </p>
+ */
+ public static final int VERBOSE = Log.VERBOSE;
+
+ /**
+ * Priority constant for the println method; use Logger.d.
+ * <p>
+ * This log level will print all logs except verbose.
+ * </p>
+ */
+ public static final int DEBUG = Log.DEBUG;
+
+ /**
+ * Priority constant for the println method; use Logger.i.
+ * <p>
+ * This log level will print all logs except verbose and debug.
+ * </p>
+ */
+ public static final int INFO = Log.INFO;
+
+ /**
+ * Priority constant for the println method; use Logger.w.
+ * <p>
+ * This log level will print only warn and error logs.
+ * </p>
+ */
+ public static final int WARN = Log.WARN;
+
+ /**
+ * Priority constant for the println method; use Logger.e.
+ * <p>
+ * This log level will print only error logs.
+ * </p>
+ */
+ public static final int ERROR = Log.ERROR;
+
+ /**
+ * Priority constant for the println method.
+ * <p>
+ * This log level won't print any logs.
+ * </p>
+ */
+ public static final int NONE = 99;
+
+ /**
+ * Log level indicates which logs are allowed to be emitted by the Mapbox Maps SDK for Android.
+ */
+ @IntDef( {VERBOSE, DEBUG, INFO, WARN, ERROR, NONE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LogLevel {
+ }
} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java
index 17d3ab0aa2..3f24ebe2ac 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java
@@ -344,8 +344,10 @@ final class MapGestureDetector {
}
if (motionEvent.getActionMasked() == MotionEvent.ACTION_UP) {
- // re-enabled the move detector
- gesturesManager.getMoveGestureDetector().setEnabled(true);
+ if (executeDoubleTap) {
+ // re-enable the move detector only if we did not start the quickzoom, otherwise, re-enable in the #onScaleEnd
+ gesturesManager.getMoveGestureDetector().setEnabled(true);
+ }
if (!uiSettings.isZoomGesturesEnabled() || !uiSettings.isDoubleTapGesturesEnabled() || !executeDoubleTap) {
return false;
@@ -512,6 +514,13 @@ final class MapGestureDetector {
@Override
public void onScaleEnd(@NonNull StandardScaleGestureDetector detector, float velocityX, float velocityY) {
+ if (quickZoom) {
+ // re-enabled the move detector only if the quickzoom happened
+ // we need to split the responsibility of re-enabling the move detector,
+ // because the double tap event (where the detector is disabled) can be canceled without warning (see #14598)
+ gesturesManager.getMoveGestureDetector().setEnabled(true);
+ }
+
if (uiSettings.isIncreaseRotateThresholdWhenScaling()) {
// resetting default angle threshold
gesturesManager.getRotateGestureDetector().setAngleThreshold(
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/http/HttpRequestImpl.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/http/HttpRequestImpl.java
index 14b76e4fb7..24cb353d24 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/http/HttpRequestImpl.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/http/HttpRequestImpl.java
@@ -182,9 +182,15 @@ public class HttpRequestImpl implements HttpRequest {
@NonNull
private static Dispatcher getDispatcher() {
Dispatcher dispatcher = new Dispatcher();
- // Matches core limit set on
- // https://github.com/mapbox/mapbox-gl-native/blob/master/platform/android/src/http_file_source.cpp#L192
- dispatcher.setMaxRequestsPerHost(20);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ // Matches core limit set on
+ // https://github.com/mapbox/mapbox-gl-native/blob/master/platform/android/src/http_file_source.cpp#L192
+ dispatcher.setMaxRequestsPerHost(20);
+ } else {
+ // Limiting concurrent request on Android 4.4, to limit impact of SSL handshake platform library crash
+ // https://github.com/mapbox/mapbox-gl-native/issues/14910
+ dispatcher.setMaxRequestsPerHost(10);
+ }
return dispatcher;
}
} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineManager.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineManager.java
index 535107c529..5bd0dd4bf6 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineManager.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineManager.java
@@ -4,11 +4,10 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
-import android.support.annotation.AnyThread;
import android.support.annotation.Keep;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
-
import com.mapbox.mapboxsdk.LibraryLoader;
import com.mapbox.mapboxsdk.Mapbox;
import com.mapbox.mapboxsdk.R;
@@ -54,7 +53,7 @@ public class OfflineManager {
private final FileSource fileSource;
// Makes sure callbacks come back to the main thread
- private Handler handler;
+ private final Handler handler = new Handler(Looper.getMainLooper());
// This object is implemented as a singleton
@SuppressLint("StaticFieldLeak")
@@ -157,15 +156,6 @@ public class OfflineManager {
return instance;
}
- @AnyThread
- private Handler getHandler() {
- if (handler == null) {
- handler = new Handler(Looper.getMainLooper());
- }
-
- return handler;
- }
-
/**
* Retrieve all regions in the offline database.
* <p>
@@ -181,7 +171,7 @@ public class OfflineManager {
@Override
public void onList(final OfflineRegion[] offlineRegions) {
- getHandler().post(new Runnable() {
+ handler.post(new Runnable() {
@Override
public void run() {
fileSource.deactivate();
@@ -192,7 +182,7 @@ public class OfflineManager {
@Override
public void onError(final String error) {
- getHandler().post(new Runnable() {
+ handler.post(new Runnable() {
@Override
public void run() {
fileSource.deactivate();
@@ -234,7 +224,7 @@ public class OfflineManager {
public void run() {
String errorMessage = null;
if (src.canWrite()) {
- getHandler().post(new Runnable() {
+ handler.post(new Runnable() {
@Override
public void run() {
// path writable, merge and update schema in place if necessary
@@ -246,7 +236,7 @@ public class OfflineManager {
final File dst = new File(FileSource.getInternalCachePath(context), src.getName());
try {
copyTempDatabaseFile(src, dst);
- getHandler().post(new Runnable() {
+ handler.post(new Runnable() {
@Override
public void run() {
// merge and update schema using the copy
@@ -264,7 +254,7 @@ public class OfflineManager {
if (errorMessage != null) {
final String finalErrorMessage = errorMessage;
- getHandler().post(new Runnable() {
+ handler.post(new Runnable() {
@Override
public void run() {
callback.onError(finalErrorMessage);
@@ -275,6 +265,219 @@ public class OfflineManager {
}).start();
}
+ /**
+ * Delete existing database and re-initialize.
+ * <p>
+ * When the operation is complete or encounters an error, the given callback will be
+ * executed on the database thread; it is the responsibility of the SDK bindings
+ * to re-execute a user-provided callback on the main thread.
+ * </p>
+ *
+ * @param callback the callback to be invoked when the database was reset or when the operation erred.
+ */
+ public void resetDatabase(@Nullable final FileSourceCallback callback) {
+ fileSource.activate();
+ nativeResetDatabase(new FileSourceCallback() {
+ @Override
+ public void onSuccess() {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ fileSource.deactivate();
+ if (callback != null) {
+ callback.onSuccess();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onError(@NonNull final String message) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ fileSource.deactivate();
+ if (callback != null) {
+ callback.onError(message);
+ }
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * Forces revalidation of the ambient cache.
+ * <p>
+ * Forces Mapbox GL Native to revalidate resources stored in the ambient
+ * cache with the tile server before using them, making sure they
+ * are the latest version. This is more efficient than cleaning the
+ * cache because if the resource is considered valid after the server
+ * lookup, it will not get downloaded again.
+ * <p>
+ * Resources overlapping with offline regions will not be affected
+ * by this call.
+ * </p>
+ *
+ * @param callback the callback to be invoked when the ambient cache was invalidated or when the operation erred.
+ */
+ public void invalidateAmbientCache(@Nullable final FileSourceCallback callback) {
+ fileSource.activate();
+ nativeInvalidateAmbientCache(new FileSourceCallback() {
+ @Override
+ public void onSuccess() {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ fileSource.deactivate();
+ if (callback != null) {
+ callback.onSuccess();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onError(@NonNull final String message) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ fileSource.deactivate();
+ if (callback != null) {
+ callback.onError(message);
+ }
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * Erase resources from the ambient cache, freeing storage space.
+ * <p>
+ * Erases the ambient cache, freeing resources. This operation can be
+ * potentially slow because it will trigger a VACUUM on SQLite,
+ * forcing the database to move pages on the filesystem.
+ * </p>
+ * <p>
+ * Resources overlapping with offline regions will not be affected
+ * by this call.
+ * </p>
+ *
+ * @param callback the callback to be invoked when the ambient cache was cleared or when the operation erred.
+ */
+ public void clearAmbientCache(@Nullable final FileSourceCallback callback) {
+ fileSource.activate();
+ nativeClearAmbientCache(new FileSourceCallback() {
+ @Override
+ public void onSuccess() {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ fileSource.deactivate();
+ if (callback != null) {
+ callback.onSuccess();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onError(@NonNull final String message) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ fileSource.deactivate();
+ if (callback != null) {
+ callback.onError(message);
+ }
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * Sets the maximum size in bytes for the ambient cache.
+ * <p>
+ * This call is potentially expensive because it will try
+ * to trim the data in case the database is larger than the
+ * size defined. The size of offline regions are not affected
+ * by this settings, but the ambient cache will always try
+ * to not exceed the maximum size defined, taking into account
+ * the current size for the offline regions.
+ * </p>
+ * <p>
+ * Note that if you use the SDK's offline functionality, your ability to set the ambient cache size will be limited.
+ * Space that offline regions take up detract from the space available for ambient caching, and the ambient cache
+ * size does not block offline downloads. For example: if the maximum cache size is set to 50 MB and 40 MB are
+ * already used by offline regions, the ambient cache size will effectively be 10 MB.
+ * </p>
+ * <p>
+ * Setting the size to 0 will disable the cache if there is no
+ * offline region on the database.
+ * </p>
+ * <[
+ * <p>
+ * This method should always be called at the start of an app, before setting the style and loading a map.
+ * Otherwise, the map will instantiate with the default cache size of 50 MB.
+ * </p>
+ *
+ * @param size the maximum size of the ambient cache
+ * @param callback the callback to be invoked when the the maximum size has been set or when the operation erred.
+ */
+ public void setMaximumAmbientCacheSize(long size, @Nullable final FileSourceCallback callback) {
+ fileSource.activate();
+ nativeSetMaximumAmbientCacheSize(size, new FileSourceCallback() {
+ @Override
+ public void onSuccess() {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ fileSource.deactivate();
+ if (callback != null) {
+ callback.onSuccess();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onError(@NonNull final String message) {
+ fileSource.activate();
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ fileSource.deactivate();
+ if (callback != null) {
+ callback.onError(message);
+ }
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * This callback receives an asynchronous response indicating if an operation has succeeded or failed.
+ */
+ @Keep
+ public interface FileSourceCallback {
+
+ /**
+ * Receives the success of an operation
+ */
+ void onSuccess();
+
+ /**
+ * Receives an error message if an operation was not successful
+ *
+ * @param message the error message
+ */
+ void onError(@NonNull String message);
+
+ }
+
private static void copyTempDatabaseFile(@NonNull File sourceFile, File destFile) throws IOException {
if (!destFile.exists() && !destFile.createNewFile()) {
throw new IOException("Unable to copy database file for merge.");
@@ -308,7 +511,7 @@ public class OfflineManager {
if (isTemporaryFile) {
file.delete();
}
- getHandler().post(new Runnable() {
+ handler.post(new Runnable() {
@Override
public void run() {
fileSource.deactivate();
@@ -322,7 +525,7 @@ public class OfflineManager {
if (isTemporaryFile) {
file.delete();
}
- getHandler().post(new Runnable() {
+ handler.post(new Runnable() {
@Override
public void run() {
fileSource.deactivate();
@@ -365,7 +568,7 @@ public class OfflineManager {
@Override
public void onCreate(final OfflineRegion offlineRegion) {
- getHandler().post(new Runnable() {
+ handler.post(new Runnable() {
@Override
public void run() {
ConnectivityReceiver.instance(context).deactivate();
@@ -377,7 +580,7 @@ public class OfflineManager {
@Override
public void onError(final String error) {
- getHandler().post(new Runnable() {
+ handler.post(new Runnable() {
@Override
public void run() {
ConnectivityReceiver.instance(context).deactivate();
@@ -431,6 +634,18 @@ public class OfflineManager {
@Keep
private native void mergeOfflineRegions(FileSource fileSource, String path, MergeOfflineRegionsCallback callback);
+ @Keep
+ private native void nativeResetDatabase(@Nullable FileSourceCallback callback);
+
+ @Keep
+ private native void nativeInvalidateAmbientCache(@Nullable FileSourceCallback callback);
+
+ @Keep
+ private native void nativeClearAmbientCache(@Nullable FileSourceCallback callback);
+
+ @Keep
+ private native void nativeSetMaximumAmbientCacheSize(long size, @Nullable FileSourceCallback callback);
+
/**
* Insert the provided resource into the ambient cache
* This method mimics the caching that would take place if the equivalent
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegion.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegion.java
index 863219854b..2217850a2e 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegion.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegion.java
@@ -7,7 +7,6 @@ import android.support.annotation.IntDef;
import android.support.annotation.Keep;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-
import com.mapbox.mapboxsdk.LibraryLoader;
import com.mapbox.mapboxsdk.Mapbox;
import com.mapbox.mapboxsdk.net.ConnectivityReceiver;
@@ -148,6 +147,25 @@ public class OfflineRegion {
}
/**
+ * This callback receives an asynchronous response containing a notification when
+ * an offline region has been invalidated, or a {@link String} error message otherwise.
+ */
+ @Keep
+ public interface OfflineRegionInvalidateCallback {
+ /**
+ * Receives the invalidate notification
+ */
+ void onInvalidate();
+
+ /**
+ * Receives the error message
+ *
+ * @param error the error message
+ */
+ void onError(String error);
+ }
+
+ /**
* This callback receives an asynchronous response containing the newly update
* OfflineMetadata in the database, or an error message otherwise.
*/
@@ -337,14 +355,14 @@ public class OfflineRegion {
* @param callback the callback to invoked.
*/
public void getStatus(@NonNull final OfflineRegionStatusCallback callback) {
- FileSource.getInstance(Mapbox.getApplicationContext()).activate();
+ fileSource.activate();
getOfflineRegionStatus(new OfflineRegionStatusCallback() {
@Override
public void onStatus(final OfflineRegionStatus status) {
handler.post(new Runnable() {
@Override
public void run() {
- FileSource.getInstance(Mapbox.getApplicationContext()).deactivate();
+ fileSource.deactivate();
callback.onStatus(status);
}
});
@@ -355,7 +373,7 @@ public class OfflineRegion {
handler.post(new Runnable() {
@Override
public void run() {
- FileSource.getInstance(Mapbox.getApplicationContext()).deactivate();
+ fileSource.deactivate();
callback.onError(error);
}
});
@@ -383,14 +401,14 @@ public class OfflineRegion {
public void delete(@NonNull final OfflineRegionDeleteCallback callback) {
if (!isDeleted) {
isDeleted = true;
- FileSource.getInstance(Mapbox.getApplicationContext()).activate();
+ fileSource.activate();
deleteOfflineRegion(new OfflineRegionDeleteCallback() {
@Override
public void onDelete() {
handler.post(new Runnable() {
@Override
public void run() {
- FileSource.getInstance(Mapbox.getApplicationContext()).deactivate();
+ fileSource.deactivate();
callback.onDelete();
OfflineRegion.this.finalize();
}
@@ -403,7 +421,7 @@ public class OfflineRegion {
@Override
public void run() {
isDeleted = false;
- FileSource.getInstance(Mapbox.getApplicationContext()).deactivate();
+ fileSource.deactivate();
callback.onError(error);
}
});
@@ -413,6 +431,46 @@ public class OfflineRegion {
}
/**
+ * Invalidate all the tiles from an offline region forcing Mapbox GL to revalidate
+ * the tiles with the server before using. This is more efficient than deleting the
+ * offline region and downloading it again because if the data on the cache matches
+ * the server, no new data gets transmitted.
+ *
+ * @param callback the callback to be invoked
+ */
+ public void invalidate(@Nullable final OfflineRegionInvalidateCallback callback) {
+ fileSource.activate();
+ invalidateOfflineRegion(new OfflineRegionInvalidateCallback() {
+
+ @Override
+ public void onInvalidate() {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ fileSource.deactivate();
+ if (callback != null) {
+ callback.onInvalidate();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onError(@NonNull final String message) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ fileSource.deactivate();
+ if (callback != null) {
+ callback.onError(message);
+ }
+ }
+ });
+ }
+ });
+ }
+
+ /**
* Update an offline region metadata from the database.
* <p>
* When the operation is complete or encounters an error, the given callback will be
@@ -469,4 +527,7 @@ public class OfflineRegion {
@Keep
private native void updateOfflineRegionMetadata(byte[] metadata, OfflineRegionUpdateMetadataCallback callback);
+ @Keep
+ private native void invalidateOfflineRegion(OfflineRegionInvalidateCallback callback);
+
}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/storage/FileSource.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/storage/FileSource.java
index b3b7b61831..cdf197411a 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/storage/FileSource.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/storage/FileSource.java
@@ -11,7 +11,6 @@ import android.support.annotation.Keep;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
-
import com.mapbox.mapboxsdk.MapStrictMode;
import com.mapbox.mapboxsdk.Mapbox;
import com.mapbox.mapboxsdk.constants.MapboxConstants;
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/ColorUtils.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/ColorUtils.java
index ab3d68547e..8d741d1bdc 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/ColorUtils.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/ColorUtils.java
@@ -124,14 +124,16 @@ public class ColorUtils {
*/
@ColorInt
public static int rgbaToColor(@NonNull String value) {
- Pattern c = Pattern.compile("rgba?\\s*\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,?\\s*(\\d+\\.?\\d*)?\\s*\\)");
+ // we need to accept and floor float values as well, as those can come from core
+ Pattern c = Pattern.compile("rgba?\\s*\\(\\s*(\\d+\\.?\\d*)\\s*,\\s*(\\d+\\.?\\d*)\\s*,\\s*(\\d+\\.?\\d*)\\s*,"
+ + "?\\s*(\\d+\\.?\\d*)?\\s*\\)");
Matcher m = c.matcher(value);
if (m.matches() && m.groupCount() == 3) {
- return Color.rgb(Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)),
- Integer.parseInt(m.group(3)));
+ return Color.rgb((int) Float.parseFloat(m.group(1)), (int) Float.parseFloat(m.group(2)),
+ (int) Float.parseFloat(m.group(3)));
} else if (m.matches() && m.groupCount() == 4) {
- return Color.argb((int) (Float.parseFloat(m.group(4)) * 255), Integer.parseInt(m.group(1)),
- Integer.parseInt(m.group(2)), Integer.parseInt(m.group(3)));
+ return Color.argb((int) (Float.parseFloat(m.group(4)) * 255), (int) Float.parseFloat(m.group(1)),
+ (int) Float.parseFloat(m.group(2)), (int) Float.parseFloat(m.group(3)));
} else {
throw new ConversionException("Not a valid rgb/rgba value");
}
diff --git a/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt
index 1bc92ed7e3..0091c50b08 100644
--- a/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt
+++ b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt
@@ -403,6 +403,12 @@ class LocationAnimatorCoordinatorTest {
}
@Test
+ fun resetAllLayerAnimations_empty() {
+ locationAnimatorCoordinator.resetAllLayerAnimations()
+ assertTrue(locationAnimatorCoordinator.animatorArray.size() == 0)
+ }
+
+ @Test
fun addNewListener() {
val listener = Mockito.mock(AnimationsValueChangeListener::class.java)
val holder = AnimatorListenerHolder(RenderMode.NORMAL, listener)
diff --git a/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentTest.kt b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentTest.kt
index 384a8cf65a..2994c29d5b 100644
--- a/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentTest.kt
+++ b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentTest.kt
@@ -15,7 +15,6 @@ import com.mapbox.mapboxsdk.location.modes.RenderMode
import com.mapbox.mapboxsdk.maps.MapboxMap
import com.mapbox.mapboxsdk.maps.Style
import com.mapbox.mapboxsdk.maps.Transform
-import io.mockk.mockk
import org.junit.Assert
import org.junit.Before
import org.junit.Test
@@ -88,7 +87,7 @@ class LocationComponentTest {
@Test
fun activateWithRequestTest() {
- locationComponent.activateLocationComponent(context, mockk(), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
Assert.assertEquals(locationEngineRequest, locationComponent.locationEngineRequest)
@@ -102,27 +101,27 @@ class LocationComponentTest {
.getDimension(R.dimen.mapbox_locationComponentTrackingMultiFingerMoveThreshold)
doReturn(0f).`when`(resources)
.getDimension(R.dimen.mapbox_locationComponentTrackingMultiFingerMoveThreshold)
- locationComponent.activateLocationComponent(context, mockk(), true, locationEngineRequest)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), true, locationEngineRequest)
Assert.assertEquals(locationEngineRequest, locationComponent.locationEngineRequest)
}
@Test
fun activateWithDefaultLocationEngineRequestAndOptionsTestDefaultLocationEngine() {
- locationComponent.activateLocationComponent(context, mockk(), true, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), true, locationEngineRequest, locationComponentOptions)
Assert.assertEquals(locationEngineRequest, locationComponent.locationEngineRequest)
Assert.assertNotNull(locationComponent.locationEngine)
}
@Test
fun activateWithDefaultLocationEngineRequestAndOptionsTestCustomLocationEngine() {
- locationComponent.activateLocationComponent(context, mockk(), false, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), false, locationEngineRequest, locationComponentOptions)
Assert.assertEquals(locationEngineRequest, locationComponent.locationEngineRequest)
Assert.assertNull(locationComponent.locationEngine)
}
@Test
fun locationUpdatesWhenEnabledDisableTest() {
- locationComponent.activateLocationComponent(context, mockk(), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
verify(locationEngine, times(0)).removeLocationUpdates(currentListener)
verify(locationEngine, times(0)).requestLocationUpdates(eq(locationEngineRequest), eq(currentListener), any(Looper::class.java))
@@ -140,7 +139,7 @@ class LocationComponentTest {
@Test
fun locationUpdatesWhenStartedStoppedTest() {
- locationComponent.activateLocationComponent(context, mockk(), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
locationComponent.onStart()
locationComponent.isLocationComponentEnabled = true
@@ -153,7 +152,7 @@ class LocationComponentTest {
@Test
fun locationUpdatesWhenNewRequestTest() {
- locationComponent.activateLocationComponent(context, mockk(), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
locationComponent.onStart()
locationComponent.isLocationComponentEnabled = true
@@ -165,7 +164,7 @@ class LocationComponentTest {
@Test
fun lastLocationUpdateOnStartTest() {
- locationComponent.activateLocationComponent(context, mockk(), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
locationComponent.onStart()
locationComponent.isLocationComponentEnabled = true
@@ -174,7 +173,7 @@ class LocationComponentTest {
@Test
fun transitionCallbackFinishedTest() {
- locationComponent.activateLocationComponent(context, mockk(), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
locationComponent.onStart()
locationComponent.isLocationComponentEnabled = true
`when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT)
@@ -192,7 +191,7 @@ class LocationComponentTest {
@Test
fun transitionCallbackCanceledTest() {
- locationComponent.activateLocationComponent(context, mockk(), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
locationComponent.onStart()
locationComponent.isLocationComponentEnabled = true
`when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT)
@@ -210,7 +209,7 @@ class LocationComponentTest {
@Test
fun transitionCustomFinishedTest() {
- locationComponent.activateLocationComponent(context, mockk(), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
locationComponent.onStart()
locationComponent.isLocationComponentEnabled = true
`when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT)
@@ -228,7 +227,7 @@ class LocationComponentTest {
@Test
fun compass_listenWhenConsumedByNoneCamera() {
- locationComponent.activateLocationComponent(context, mockk(), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
locationComponent.onStart()
locationComponent.isLocationComponentEnabled = true
`when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT)
@@ -240,7 +239,7 @@ class LocationComponentTest {
@Test
fun compass_listenWhenConsumedByTrackingCamera() {
- locationComponent.activateLocationComponent(context, mockk(), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
locationComponent.onStart()
locationComponent.isLocationComponentEnabled = true
`when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT)
@@ -252,7 +251,7 @@ class LocationComponentTest {
@Test
fun compass_listenWhenConsumedByLayer() {
- locationComponent.activateLocationComponent(context, mockk(), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
locationComponent.onStart()
locationComponent.isLocationComponentEnabled = true
`when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT)
@@ -264,7 +263,7 @@ class LocationComponentTest {
@Test
fun compass_notListenWhenNotConsumed() {
- locationComponent.activateLocationComponent(context, mockk(), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
locationComponent.onStart()
locationComponent.isLocationComponentEnabled = true
`when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT)
@@ -283,7 +282,7 @@ class LocationComponentTest {
@Test
fun compass_removeListenerOnChange() {
- locationComponent.activateLocationComponent(context, mockk(), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
locationComponent.onStart()
locationComponent.isLocationComponentEnabled = true
`when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT)
@@ -297,7 +296,7 @@ class LocationComponentTest {
@Test
fun compass_removeListenerOnStop() {
- locationComponent.activateLocationComponent(context, mockk(), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
locationComponent.onStart()
locationComponent.isLocationComponentEnabled = true
`when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT)
@@ -310,7 +309,7 @@ class LocationComponentTest {
@Test
fun compass_reAddListenerOnStart() {
- locationComponent.activateLocationComponent(context, mockk(), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
locationComponent.onStart()
locationComponent.isLocationComponentEnabled = true
`when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT)
@@ -324,7 +323,7 @@ class LocationComponentTest {
@Test
fun compass_removeListenerOnStyleStartLoad() {
- locationComponent.activateLocationComponent(context, mockk(), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
locationComponent.onStart()
locationComponent.isLocationComponentEnabled = true
`when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT)
@@ -337,7 +336,7 @@ class LocationComponentTest {
@Test
fun compass_reAddListenerOnStyleLoadFinished() {
- locationComponent.activateLocationComponent(context, mockk(), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
locationComponent.onStart()
locationComponent.isLocationComponentEnabled = true
`when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT)
@@ -351,7 +350,7 @@ class LocationComponentTest {
@Test
fun compass_reAddListenerOnlyWhenEnabled() {
- locationComponent.activateLocationComponent(context, mockk(), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
locationComponent.onStart()
locationComponent.isLocationComponentEnabled = true
`when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT)
@@ -367,7 +366,7 @@ class LocationComponentTest {
@Test
fun compass_notAdListenerWhenDisabled() {
- locationComponent.activateLocationComponent(context, mockk(), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
locationComponent.onStart()
`when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT)
@@ -378,7 +377,7 @@ class LocationComponentTest {
@Test
fun compass_notAdListenerWhenStopped() {
- locationComponent.activateLocationComponent(context, mockk(), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
locationComponent.isLocationComponentEnabled = true
`when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT)
@@ -389,11 +388,66 @@ class LocationComponentTest {
@Test
fun developerAnimationCalled() {
- locationComponent.activateLocationComponent(context, mockk(), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
locationComponent.isLocationComponentEnabled = true
for (listener in developerAnimationListeners) {
listener.onDeveloperAnimationStarted()
}
verify(locationCameraController).setCameraMode(eq(CameraMode.NONE), isNull<Location>(), eq(TRANSITION_ANIMATION_DURATION_MS), isNull<Double>(), isNull<Double>(), isNull<Double>(), any())
}
+
+ @Test
+ fun internal_cameraTrackingChangedListener_onCameraTrackingDismissed() {
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.isLocationComponentEnabled = true
+
+ val cameraChangeListener: OnCameraTrackingChangedListener = mock(OnCameraTrackingChangedListener::class.java)
+ locationComponent.addOnCameraTrackingChangedListener(cameraChangeListener)
+
+ locationComponent.cameraTrackingChangedListener.onCameraTrackingDismissed()
+
+ verify(cameraChangeListener).onCameraTrackingDismissed()
+ }
+
+ @Test
+ fun internal_cameraTrackingChangedListener_onCameraTrackingChanged() {
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.isLocationComponentEnabled = true
+
+ val cameraValueListener: AnimatorListenerHolder = mock(AnimatorListenerHolder::class.java)
+ val layerValueListener: AnimatorListenerHolder = mock(AnimatorListenerHolder::class.java)
+ `when`(locationCameraController.animationListeners).thenReturn(setOf(cameraValueListener))
+ `when`(locationLayerController.animationListeners).thenReturn(setOf(layerValueListener))
+ val cameraChangeListener: OnCameraTrackingChangedListener = mock(OnCameraTrackingChangedListener::class.java)
+ locationComponent.addOnCameraTrackingChangedListener(cameraChangeListener)
+
+ locationComponent.cameraTrackingChangedListener.onCameraTrackingChanged(CameraMode.TRACKING_GPS)
+
+ verify(locationAnimatorCoordinator).cancelZoomAnimation()
+ verify(locationAnimatorCoordinator).cancelTiltAnimation()
+ verify(locationAnimatorCoordinator).updateAnimatorListenerHolders(eq(setOf(cameraValueListener, layerValueListener)))
+ verify(locationAnimatorCoordinator).resetAllCameraAnimations(any(), anyBoolean())
+ verify(locationAnimatorCoordinator).resetAllLayerAnimations()
+ verify(cameraChangeListener).onCameraTrackingChanged(CameraMode.TRACKING_GPS)
+ }
+
+ @Test
+ fun internal_renderModeChangedListener_onRenderModeChanged() {
+ locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions)
+ locationComponent.isLocationComponentEnabled = true
+
+ val cameraListener: AnimatorListenerHolder = mock(AnimatorListenerHolder::class.java)
+ val layerListener: AnimatorListenerHolder = mock(AnimatorListenerHolder::class.java)
+ `when`(locationCameraController.animationListeners).thenReturn(setOf(cameraListener))
+ `when`(locationLayerController.animationListeners).thenReturn(setOf(layerListener))
+ val renderChangeListener: OnRenderModeChangedListener = mock(OnRenderModeChangedListener::class.java)
+ locationComponent.addOnRenderModeChangedListener(renderChangeListener)
+
+ locationComponent.renderModeChangedListener.onRenderModeChanged(RenderMode.NORMAL)
+
+ verify(locationAnimatorCoordinator).updateAnimatorListenerHolders(eq(setOf(cameraListener, layerListener)))
+ verify(locationAnimatorCoordinator).resetAllCameraAnimations(any(), anyBoolean())
+ verify(locationAnimatorCoordinator).resetAllLayerAnimations()
+ verify(renderChangeListener).onRenderModeChanged(RenderMode.NORMAL)
+ }
} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/log/LoggerTest.kt b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/log/LoggerTest.kt
new file mode 100644
index 0000000000..8b55419ea4
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/log/LoggerTest.kt
@@ -0,0 +1,93 @@
+package com.mapbox.mapboxsdk.log
+
+import io.mockk.mockk
+import io.mockk.verify
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class LoggerTest {
+
+ private val logger: LoggerDefinition = mockk(relaxed = true)
+
+ @Before
+ fun setUp() {
+ Logger.setLoggerDefinition(logger)
+ }
+
+ @Test
+ fun verbosityLogLevel() {
+ Logger.setVerbosity(Logger.VERBOSE)
+ Logger.v(TAG, MESSAGE)
+ verify { logger.v(TAG, MESSAGE) }
+ }
+
+ @Test
+ fun verbosityLogLevelIgnore() {
+ Logger.setVerbosity(Logger.DEBUG)
+ Logger.v(TAG, MESSAGE)
+ verify(exactly = 0){ logger.v(TAG, MESSAGE) }
+ }
+
+ @Test
+ fun debugLogLevel() {
+ Logger.setVerbosity(Logger.VERBOSE)
+ Logger.d(TAG, MESSAGE)
+ verify { logger.d(TAG, MESSAGE) }
+ }
+
+ @Test
+ fun debugLogLevelIgnore() {
+ Logger.setVerbosity(Logger.WARN)
+ Logger.d(TAG, MESSAGE)
+ verify(exactly = 0){ logger.d(TAG, MESSAGE) }
+ }
+
+ @Test
+ fun warnLogLevel() {
+ Logger.setVerbosity(Logger.WARN)
+ Logger.w(TAG, MESSAGE)
+ verify { logger.w(TAG, MESSAGE) }
+ }
+
+ @Test
+ fun warnLogLevelIgnore() {
+ Logger.setVerbosity(Logger.ERROR)
+ Logger.w(TAG, MESSAGE)
+ verify(exactly = 0){ logger.w(TAG, MESSAGE) }
+ }
+
+ @Test
+ fun errorLogLevel() {
+ Logger.setVerbosity(Logger.ERROR)
+ Logger.e(TAG, MESSAGE)
+ verify { logger.e(TAG, MESSAGE) }
+ }
+
+ @Test
+ fun errorLogLevelIgnore() {
+ Logger.setVerbosity(Logger.NONE)
+ Logger.e(TAG, MESSAGE)
+ verify(exactly = 0){ logger.e(TAG, MESSAGE) }
+ }
+
+ @Test
+ fun noneLogLevelIgnore() {
+ Logger.setVerbosity(Logger.NONE)
+ Logger.v(TAG, MESSAGE)
+ Logger.d(TAG, MESSAGE)
+ Logger.w(TAG, MESSAGE)
+ Logger.e(TAG, MESSAGE)
+ verify(exactly = 0){ logger.v(TAG, MESSAGE) }
+ verify(exactly = 0){ logger.d(TAG, MESSAGE) }
+ verify(exactly = 0){ logger.w(TAG, MESSAGE) }
+ verify(exactly = 0){ logger.e(TAG, MESSAGE) }
+ }
+
+ companion object {
+ const val TAG: String = "TAG"
+ const val MESSAGE: String = "message"
+ }
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/utils/ColorUtilsTest.kt b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/utils/ColorUtilsTest.kt
new file mode 100644
index 0000000000..530a08a4e5
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/utils/ColorUtilsTest.kt
@@ -0,0 +1,22 @@
+package com.mapbox.mapboxsdk.utils
+
+import android.graphics.Color
+import junit.framework.Assert
+import org.junit.Test
+
+class ColorUtilsTest {
+
+ @Test
+ fun rgbaToColor_decimalComponent() {
+ val input = "rgba(255,128.0000952303,0,0.7)"
+ val result = ColorUtils.rgbaToColor(input)
+ Assert.assertEquals(Color.argb(255, 128, 0, (0.7 * 255).toInt()), result)
+ }
+
+ @Test
+ fun rgbaToColor_decimalComponent_floor() {
+ val input = "rgba(255,128.70123,0,0.7)"
+ val result = ColorUtils.rgbaToColor(input)
+ Assert.assertEquals(Color.argb(255, 128, 0, (0.7 * 255).toInt()), result)
+ }
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/build.gradle b/platform/android/MapboxGLAndroidSDKTestApp/build.gradle
index ca280436ea..bcdafd55d5 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/build.gradle
+++ b/platform/android/MapboxGLAndroidSDKTestApp/build.gradle
@@ -79,6 +79,7 @@ dependencies {
androidTestImplementation dependenciesList.testEspressoIntents
androidTestImplementation dependenciesList.testEspressoContrib
androidTestImplementation dependenciesList.testUiAutomator
+ androidTestImplementation dependenciesList.appCenter
}
apply from: "${rootDir}/gradle/gradle-make.gradle"
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/AppCenter.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/AppCenter.kt
new file mode 100644
index 0000000000..fb946a9c31
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/AppCenter.kt
@@ -0,0 +1,16 @@
+package com.mapbox.mapboxsdk
+
+import com.microsoft.appcenter.espresso.Factory
+import org.junit.After
+import org.junit.Rule
+
+abstract class AppCenter {
+ @get:Rule
+ var reportHelper = Factory.getReportHelper()!!
+
+
+ @After
+ open fun afterTest() {
+ reportHelper.label(javaClass.simpleName)
+ }
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/camera/CameraUpdateFactoryTest.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/camera/CameraUpdateFactoryTest.kt
new file mode 100644
index 0000000000..446b731a61
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/camera/CameraUpdateFactoryTest.kt
@@ -0,0 +1,144 @@
+package com.mapbox.mapboxsdk.camera
+
+import android.support.test.runner.AndroidJUnit4
+import com.mapbox.mapboxsdk.geometry.LatLng
+import com.mapbox.mapboxsdk.geometry.LatLngBounds
+import com.mapbox.mapboxsdk.testapp.activity.BaseTest
+import com.mapbox.mapboxsdk.testapp.activity.espresso.DeviceIndependentTestActivity
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class CameraUpdateFactoryTest : BaseTest() {
+
+ override fun getActivityClass(): Class<*> {
+ return DeviceIndependentTestActivity::class.java
+ }
+
+ @Test
+ fun testLatLngBoundsUntiltedUnrotated() {
+ rule.runOnUiThread {
+ mapboxMap.cameraPosition = CameraPosition.Builder()
+ .target(LatLng(60.0, 24.0))
+ .bearing(0.0)
+ .tilt(0.0)
+ .build()
+
+ val bounds: LatLngBounds = LatLngBounds.Builder()
+ .include(LatLng(62.0, 26.0))
+ .include(LatLng(58.0, 22.0))
+ .build()
+
+ mapboxMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0))
+
+ val cameraPosition = mapboxMap.cameraPosition
+ assertEquals("latitude should match:", 60.0, cameraPosition.target.latitude, 0.1)
+ assertEquals("longitude should match:", 24.0, cameraPosition.target.longitude, 0.1)
+ assertEquals("bearing should match:", 0.0, cameraPosition.bearing, 0.1)
+ assertEquals("zoom should match", 5.5, cameraPosition.zoom, 0.1)
+ assertEquals("tilt should match:", 0.0, cameraPosition.tilt, 0.1)
+ }
+ }
+
+ @Test
+ fun testLatLngBoundsTilted() {
+ rule.runOnUiThread {
+ mapboxMap.cameraPosition = CameraPosition.Builder()
+ .target(LatLng(60.0, 24.0))
+ .bearing(0.0)
+ .tilt(45.0)
+ .build()
+
+ val bounds: LatLngBounds = LatLngBounds.Builder()
+ .include(LatLng(62.0, 26.0))
+ .include(LatLng(58.0, 22.0))
+ .build()
+
+ mapboxMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0))
+
+ val cameraPosition = mapboxMap.cameraPosition
+ assertEquals("latitude should match:", 60.0, cameraPosition.target.latitude, 0.1)
+ assertEquals("longitude should match:", 24.0, cameraPosition.target.longitude, 0.1)
+ assertEquals("bearing should match:", 0.0, cameraPosition.bearing, 0.1)
+ assertEquals("zoom should match", 6.0, cameraPosition.zoom, 0.1)
+ assertEquals("tilt should match:", 45.0, cameraPosition.tilt, 0.1)
+ }
+ }
+
+ @Test
+ fun testLatLngBoundsRotated() {
+ rule.runOnUiThread {
+ mapboxMap.cameraPosition = CameraPosition.Builder()
+ .target(LatLng(60.0, 24.0))
+ .bearing(30.0)
+ .tilt(0.0)
+ .build()
+
+ val bounds: LatLngBounds = LatLngBounds.Builder()
+ .include(LatLng(62.0, 26.0))
+ .include(LatLng(58.0, 22.0))
+ .build()
+
+ mapboxMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0))
+
+ val cameraPosition = mapboxMap.cameraPosition
+ assertEquals("latitude should match:", 60.0, cameraPosition.target.latitude, 0.1)
+ assertEquals("longitude should match:", 24.0, cameraPosition.target.longitude, 0.1)
+ assertEquals("bearing should match:", 30.0, cameraPosition.bearing, 0.1)
+ assertEquals("zoom should match", 5.3, cameraPosition.zoom, 0.1)
+ assertEquals("tilt should match:", 0.0, cameraPosition.tilt, 0.1)
+ }
+ }
+
+ @Test
+ fun testLatLngBoundsTiltedRotated() {
+ rule.runOnUiThread {
+ mapboxMap.cameraPosition = CameraPosition.Builder()
+ .target(LatLng(60.0, 24.0))
+ .bearing(30.0)
+ .tilt(45.0)
+ .build()
+
+ val bounds: LatLngBounds = LatLngBounds.Builder()
+ .include(LatLng(62.0, 26.0))
+ .include(LatLng(58.0, 22.0))
+ .build()
+
+ mapboxMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0))
+
+ val cameraPosition = mapboxMap.cameraPosition
+ assertEquals("latitude should match:", 60.0, cameraPosition.target.latitude, 0.1)
+ assertEquals("longitude should match:", 24.0, cameraPosition.target.longitude, 0.1)
+ assertEquals("bearing should match:", 30.0, cameraPosition.bearing, 0.1)
+ assertEquals("zoom should match", 5.6, cameraPosition.zoom, 0.1)
+ assertEquals("tilt should match:", 45.0, cameraPosition.tilt, 0.1)
+ }
+ }
+
+ @Test
+ fun testLatLngBoundsWithProvidedTiltAndRotation() {
+ rule.runOnUiThread {
+ mapboxMap.cameraPosition = CameraPosition.Builder()
+ .target(LatLng(60.0, 24.0))
+ .bearing(0.0)
+ .tilt(0.0)
+ .build()
+
+ val bounds: LatLngBounds = LatLngBounds.Builder()
+ .include(LatLng(62.0, 26.0))
+ .include(LatLng(58.0, 22.0))
+ .build()
+
+ mapboxMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 30.0, 40.0, 0))
+
+ val cameraPosition = mapboxMap.cameraPosition
+ assertEquals("latitude should match:", 60.0, cameraPosition.target.latitude, 0.1)
+ assertEquals("longitude should match:", 24.0, cameraPosition.target.longitude, 0.1)
+ assertEquals("bearing should match:", 30.0, cameraPosition.bearing, 0.1)
+ assertEquals("zoom should match", 5.6, cameraPosition.zoom, 0.1)
+ assertEquals("tilt should match:", 40.0, cameraPosition.tilt, 0.1)
+ }
+ }
+
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/integration/BaseIntegrationTest.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/integration/BaseIntegrationTest.kt
index aeb8863790..554ab644b6 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/integration/BaseIntegrationTest.kt
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/integration/BaseIntegrationTest.kt
@@ -5,11 +5,12 @@ import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.support.test.InstrumentationRegistry
import android.support.test.uiautomator.*
+import com.mapbox.mapboxsdk.AppCenter
import org.junit.Before
const val TIMEOUT_UI_SEARCH_WAIT = 5000L
-abstract class BaseIntegrationTest {
+abstract class BaseIntegrationTest : AppCenter(){
protected lateinit var device: UiDevice
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/BaseLayerTest.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/BaseLayerTest.kt
index 9a6f9c3c26..7019129e6e 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/BaseLayerTest.kt
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/BaseLayerTest.kt
@@ -2,11 +2,12 @@ package com.mapbox.mapboxsdk.maps
import android.support.test.InstrumentationRegistry
import android.support.test.runner.AndroidJUnit4
+import com.mapbox.mapboxsdk.AppCenter
import com.mapbox.mapboxsdk.style.layers.Layer
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
-abstract class BaseLayerTest {
+abstract class BaseLayerTest : AppCenter(){
private lateinit var nativeMapView: NativeMap
companion object {
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/MapboxMapInstrumentationTest.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/MapboxMapInstrumentationTest.kt
new file mode 100644
index 0000000000..e93f54161e
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/MapboxMapInstrumentationTest.kt
@@ -0,0 +1,41 @@
+package com.mapbox.mapboxsdk.maps
+
+import android.graphics.PointF
+import android.support.test.runner.AndroidJUnit4
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory
+import com.mapbox.mapboxsdk.geometry.LatLng
+import com.mapbox.mapboxsdk.testapp.activity.BaseTest
+import com.mapbox.mapboxsdk.testapp.activity.espresso.DeviceIndependentTestActivity
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class MapboxMapInstrumentationTest : BaseTest() {
+
+ override fun getActivityClass(): Class<*> {
+ return DeviceIndependentTestActivity::class.java
+ }
+
+ @Test
+ fun setPadding_cameraInvalidated() {
+ rule.runOnUiThread {
+ mapboxMap.moveCamera(CameraUpdateFactory.newLatLngZoom(LatLng(0.0, 0.0), 10.0))
+ val initialCameraPosition = mapboxMap.cameraPosition
+ val initialPoint = mapboxMap.projection.toScreenLocation(initialCameraPosition.target)
+
+ val bottomPadding = mapView.height / 4
+ val leftPadding = mapView.width / 4
+ mapboxMap.setPadding(leftPadding, 0, 0, bottomPadding)
+
+ val resultingCameraPosition = mapboxMap.cameraPosition
+ assertArrayEquals(intArrayOf(leftPadding, 0, 0, bottomPadding), mapboxMap.padding)
+ assertEquals(initialCameraPosition, resultingCameraPosition)
+ assertEquals(
+ PointF(initialPoint.x + leftPadding / 2, initialPoint.y - bottomPadding / 2),
+ mapboxMap.projection.toScreenLocation(resultingCameraPosition.target)
+ )
+ }
+ }
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/MapboxTest.java b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/MapboxTest.java
index c8737e2802..50c10766af 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/MapboxTest.java
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/MapboxTest.java
@@ -2,6 +2,8 @@ package com.mapbox.mapboxsdk.maps;
import android.support.test.annotation.UiThreadTest;
import android.support.test.runner.AndroidJUnit4;
+
+import com.mapbox.mapboxsdk.AppCenter;
import com.mapbox.mapboxsdk.Mapbox;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -11,7 +13,7 @@ import static junit.framework.Assert.assertSame;
import static junit.framework.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
-public class MapboxTest {
+public class MapboxTest extends AppCenter {
private static final String ACCESS_TOKEN = "pk.0000000001";
private static final String ACCESS_TOKEN_2 = "pk.0000000002";
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/NativeMapViewTest.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/NativeMapViewTest.kt
index a1fe6af659..409d956a13 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/NativeMapViewTest.kt
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/NativeMapViewTest.kt
@@ -5,6 +5,7 @@ import android.graphics.PointF
import android.support.test.InstrumentationRegistry
import android.support.test.annotation.UiThreadTest
import android.support.test.runner.AndroidJUnit4
+import com.mapbox.mapboxsdk.AppCenter
import com.mapbox.mapboxsdk.camera.CameraPosition
import com.mapbox.mapboxsdk.geometry.LatLng
import com.mapbox.mapboxsdk.geometry.LatLngBounds
@@ -19,7 +20,7 @@ import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
-class NativeMapViewTest {
+class NativeMapViewTest : AppCenter() {
private lateinit var nativeMapView: NativeMap
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/VisibleRegionTest.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/VisibleRegionTest.kt
index 139695461d..24c7bc0226 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/VisibleRegionTest.kt
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/VisibleRegionTest.kt
@@ -8,8 +8,7 @@ import com.mapbox.mapboxsdk.testapp.action.MapboxMapAction.invoke
import com.mapbox.mapboxsdk.testapp.activity.BaseTest
import com.mapbox.mapboxsdk.testapp.activity.espresso.PixelTestActivity
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
+import org.junit.Assert.*
import org.junit.Test
class VisibleRegionTest : BaseTest() {
@@ -70,7 +69,7 @@ class VisibleRegionTest : BaseTest() {
val visibleRegion = mapboxMap.projection.getVisibleRegion(false)
val filtered = latLngs.filter { visibleRegion.latLngBounds.contains(it) }
- assertTrue(filtered.size == 1)
+ assertEquals(1, filtered.size)
assertTrue(filtered.contains(mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height / 2f)))
}
}
@@ -89,14 +88,18 @@ class VisibleRegionTest : BaseTest() {
mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height.toFloat()),
mapboxMap.getLatLngFromScreenCoords(0f, mapView.height.toFloat()),
mapboxMap.getLatLngFromScreenCoords(0f, mapView.height / 2f),
- mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height / 2f)
+ mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height / 2f),
+ mapboxMap.getLatLngFromScreenCoords(mapView.width / 4f, mapView.height / 2f),
+ mapboxMap.getLatLngFromScreenCoords(mapView.width * 3f / 4f, 0f)
)
mapboxMap.setPadding(mapView.width / 4, 0, 0, 0)
val visibleRegion = mapboxMap.projection.getVisibleRegion(false)
- val filtered = latLngs.filter { visibleRegion.latLngBounds.contains(it) }
- assertTrue(filtered.size == 6)
+ val filtered = latLngs.filter {
+ visibleRegion.latLngBounds.contains(it)
+ }
+ assertEquals(5, filtered.size)
assertFalse(filtered.contains(mapboxMap.getLatLngFromScreenCoords(0f, mapView.height / 2f)))
}
}
@@ -115,14 +118,16 @@ class VisibleRegionTest : BaseTest() {
mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height.toFloat()),
mapboxMap.getLatLngFromScreenCoords(0f, mapView.height.toFloat()),
mapboxMap.getLatLngFromScreenCoords(0f, mapView.height / 2f),
- mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height / 2f)
+ mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height / 2f),
+ mapboxMap.getLatLngFromScreenCoords(0f, mapView.height / 4f),
+ mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height * 3f / 4f)
)
mapboxMap.setPadding(0, mapView.height / 4, 0, 0)
val visibleRegion = mapboxMap.projection.getVisibleRegion(false)
val filtered = latLngs.filter { visibleRegion.latLngBounds.contains(it) }
- assertTrue(filtered.size == 6)
+ assertEquals(5, filtered.size)
assertFalse(filtered.contains(mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, 0f)))
}
}
@@ -141,14 +146,16 @@ class VisibleRegionTest : BaseTest() {
mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height.toFloat()),
mapboxMap.getLatLngFromScreenCoords(0f, mapView.height.toFloat()),
mapboxMap.getLatLngFromScreenCoords(0f, mapView.height / 2f),
- mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height / 2f)
+ mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height / 2f),
+ mapboxMap.getLatLngFromScreenCoords(mapView.width / 4f, mapView.height / 2f),
+ mapboxMap.getLatLngFromScreenCoords(mapView.width * 3f / 4f, 0f)
)
mapboxMap.setPadding(0, 0, mapView.width / 4, 0)
val visibleRegion = mapboxMap.projection.getVisibleRegion(false)
val filtered = latLngs.filter { visibleRegion.latLngBounds.contains(it) }
- assertTrue(filtered.size == 6)
+ assertEquals(5, filtered.size)
assertFalse(filtered.contains(mapboxMap.getLatLngFromScreenCoords(mapView.width.toFloat(), mapView.height / 2f)))
}
}
@@ -167,14 +174,16 @@ class VisibleRegionTest : BaseTest() {
mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height.toFloat()),
mapboxMap.getLatLngFromScreenCoords(0f, mapView.height.toFloat()),
mapboxMap.getLatLngFromScreenCoords(0f, mapView.height / 2f),
- mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height / 2f)
+ mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height / 2f),
+ mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height / 4f),
+ mapboxMap.getLatLngFromScreenCoords(0f, mapView.height * 3f / 4f)
)
mapboxMap.setPadding(0, 0, 0, mapView.height / 4)
val visibleRegion = mapboxMap.projection.getVisibleRegion(false)
val filtered = latLngs.filter { visibleRegion.latLngBounds.contains(it) }
- assertTrue(filtered.size == 6)
+ assertEquals(5, filtered.size)
assertFalse(filtered.contains(mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height.toFloat())))
}
}
@@ -228,7 +237,7 @@ class VisibleRegionTest : BaseTest() {
val visibleRegion = mapboxMap.projection.getVisibleRegion(false)
val filtered = latLngs.filter { visibleRegion.latLngBounds.contains(it) }
- assertTrue(filtered.size == 1)
+ assertEquals(1, filtered.size)
assertTrue(filtered.contains(mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height / 2f)))
}
}
@@ -250,14 +259,17 @@ class VisibleRegionTest : BaseTest() {
mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height.toFloat()),
mapboxMap.getLatLngFromScreenCoords(0f, mapView.height.toFloat()),
mapboxMap.getLatLngFromScreenCoords(0f, mapView.height / 2f),
- mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height / 2f)
+ mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height / 2f),
+ mapboxMap.getLatLngFromScreenCoords(mapView.width / 4f, 0f),
+ mapboxMap.getLatLngFromScreenCoords(mapView.width * 3f / 4f, mapView.height / 2f)
+ .also { it.longitude += 360 }
)
mapboxMap.setPadding(mapView.width / 4, 0, 0, 0)
val visibleRegion = mapboxMap.projection.getVisibleRegion(false)
val filtered = latLngs.filter { visibleRegion.latLngBounds.contains(it) }
- assertTrue(filtered.size == 6)
+ assertEquals(5, filtered.size)
assertFalse(filtered.contains(mapboxMap.getLatLngFromScreenCoords(0f, mapView.height / 2f)))
}
}
@@ -267,7 +279,6 @@ class VisibleRegionTest : BaseTest() {
validateTestSetup()
invoke(mapboxMap) { ui: UiController, mapboxMap: MapboxMap ->
mapboxMap.moveCamera(CameraUpdateFactory.newLatLngZoom(LatLng(0.0, 180.0), 8.0))
- ui.loopMainThreadForAtLeast(5000)
val latLngs = listOf(
mapboxMap.getLatLngFromScreenCoords(0f, 0f),
mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, 0f),
@@ -280,14 +291,17 @@ class VisibleRegionTest : BaseTest() {
mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height.toFloat()),
mapboxMap.getLatLngFromScreenCoords(0f, mapView.height.toFloat()),
mapboxMap.getLatLngFromScreenCoords(0f, mapView.height / 2f),
- mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height / 2f)
+ mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height / 2f),
+ mapboxMap.getLatLngFromScreenCoords(0f, mapView.height / 4f),
+ mapboxMap.getLatLngFromScreenCoords(mapView.width.toFloat(), mapView.height * 3f / 4f)
+ .also { it.longitude += 360 }
)
mapboxMap.setPadding(0, mapView.height / 4, 0, 0)
val visibleRegion = mapboxMap.projection.getVisibleRegion(false)
val filtered = latLngs.filter { visibleRegion.latLngBounds.contains(it) }
- assertTrue(filtered.size == 6)
+ assertEquals(5, filtered.size)
assertFalse(filtered.contains(mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, 0f)))
}
}
@@ -309,14 +323,17 @@ class VisibleRegionTest : BaseTest() {
mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height.toFloat()),
mapboxMap.getLatLngFromScreenCoords(0f, mapView.height.toFloat()),
mapboxMap.getLatLngFromScreenCoords(0f, mapView.height / 2f),
- mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height / 2f)
+ mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height / 2f),
+ mapboxMap.getLatLngFromScreenCoords(mapView.width / 4f, 0f),
+ mapboxMap.getLatLngFromScreenCoords(mapView.width * 3f / 4f, mapView.height / 2f)
+ .also { it.longitude += 360 }
)
mapboxMap.setPadding(0, 0, mapView.width / 4, 0)
val visibleRegion = mapboxMap.projection.getVisibleRegion(false)
val filtered = latLngs.filter { visibleRegion.latLngBounds.contains(it) }
- assertTrue(filtered.size == 6)
+ assertEquals(5, filtered.size)
assertFalse(filtered.contains(mapboxMap.getLatLngFromScreenCoords(mapView.width.toFloat(), mapView.height / 2f)))
}
}
@@ -338,14 +355,17 @@ class VisibleRegionTest : BaseTest() {
mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height.toFloat()),
mapboxMap.getLatLngFromScreenCoords(0f, mapView.height.toFloat()),
mapboxMap.getLatLngFromScreenCoords(0f, mapView.height / 2f),
- mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height / 2f)
+ mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height / 2f),
+ mapboxMap.getLatLngFromScreenCoords(0f, mapView.height / 4f),
+ mapboxMap.getLatLngFromScreenCoords(mapView.width.toFloat(), mapView.height * 3f / 4f)
+ .also { it.longitude += 360 }
)
mapboxMap.setPadding(0, 0, 0, mapView.height / 4)
val visibleRegion = mapboxMap.projection.getVisibleRegion(false)
val filtered = latLngs.filter { visibleRegion.latLngBounds.contains(it) }
- assertTrue(filtered.size == 6)
+ assertEquals(5, filtered.size)
assertFalse(filtered.contains(mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height.toFloat())))
}
}
@@ -355,7 +375,7 @@ class VisibleRegionTest : BaseTest() {
validateTestSetup()
invoke(mapboxMap) { _: UiController, mapboxMap: MapboxMap ->
mapboxMap.moveCamera(CameraUpdateFactory.newLatLngZoom(LatLng(0.0, 0.0), 8.0))
- val d = Math.min(mapboxMap.width, mapboxMap.height) / 4;
+ val d = Math.min(mapboxMap.width, mapboxMap.height) / 4
val latLngs = listOf(
mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f, mapView.height / 2f),
mapboxMap.getLatLngFromScreenCoords(mapView.width / 2f - d / 2f, mapView.height / 2f),
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/activity/BaseTest.java b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/activity/BaseTest.java
index c91afe9b60..054069f92c 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/activity/BaseTest.java
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/activity/BaseTest.java
@@ -3,6 +3,8 @@ package com.mapbox.mapboxsdk.testapp.activity;
import android.support.annotation.CallSuper;
import android.support.annotation.UiThread;
import android.support.test.rule.ActivityTestRule;
+
+import com.mapbox.mapboxsdk.AppCenter;
import com.mapbox.mapboxsdk.Mapbox;
import com.mapbox.mapboxsdk.maps.MapView;
import com.mapbox.mapboxsdk.maps.MapboxMap;
@@ -22,7 +24,7 @@ import static junit.framework.TestCase.assertTrue;
/**
* Base class for all Activity test hooking into an existing Activity that will load style.
*/
-public abstract class BaseTest {
+public abstract class BaseTest extends AppCenter {
private static final int WAIT_TIMEOUT = 30; //seconds
@@ -46,7 +48,7 @@ public abstract class BaseTest {
@After
@CallSuper
public void afterTest() {
- // override to add logic
+ super.afterTest();
}
@UiThread
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/fragment/MapDialogFragmentTest.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/fragment/MapDialogFragmentTest.kt
index 2731b20db7..2abee8095c 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/fragment/MapDialogFragmentTest.kt
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/fragment/MapDialogFragmentTest.kt
@@ -7,6 +7,7 @@ import android.support.test.espresso.matcher.ViewMatchers.withId
import android.support.test.filters.LargeTest
import android.support.test.rule.ActivityTestRule
import android.support.test.runner.AndroidJUnit4
+import com.mapbox.mapboxsdk.AppCenter
import com.mapbox.mapboxsdk.testapp.R
import com.mapbox.mapboxsdk.testapp.action.WaitAction
import com.mapbox.mapboxsdk.testapp.activity.maplayout.MapInDialogActivity
@@ -20,7 +21,7 @@ import org.junit.runner.RunWith
*/
@RunWith(AndroidJUnit4::class)
@LargeTest
-class MapDialogFragmentTest {
+class MapDialogFragmentTest : AppCenter() {
@get:Rule
var activityRule: ActivityTestRule<MapInDialogActivity> = ActivityTestRule(MapInDialogActivity::class.java)
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/ImageMissingTest.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/ImageMissingTest.kt
index 4de4b1d55b..539cc85035 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/ImageMissingTest.kt
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/ImageMissingTest.kt
@@ -2,6 +2,7 @@ package com.mapbox.mapboxsdk.testapp.maps
import android.support.test.rule.ActivityTestRule
import android.support.test.runner.AndroidJUnit4
+import com.mapbox.mapboxsdk.AppCenter
import com.mapbox.mapboxsdk.maps.MapView
import com.mapbox.mapboxsdk.maps.Style
import com.mapbox.mapboxsdk.testapp.R
@@ -16,7 +17,7 @@ import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
@RunWith(AndroidJUnit4::class)
-class ImageMissingTest {
+class ImageMissingTest : AppCenter(){
@Rule
@JvmField
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/RemoveUnusedImagesTest.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/RemoveUnusedImagesTest.kt
index d42494b3e9..492a75b7cf 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/RemoveUnusedImagesTest.kt
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/RemoveUnusedImagesTest.kt
@@ -3,6 +3,7 @@ package com.mapbox.mapboxsdk.testapp.maps
import android.graphics.Bitmap
import android.support.test.rule.ActivityTestRule
import android.support.test.runner.AndroidJUnit4
+import com.mapbox.mapboxsdk.AppCenter
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory
import com.mapbox.mapboxsdk.geometry.LatLng
import com.mapbox.mapboxsdk.maps.MapView
@@ -20,7 +21,7 @@ import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
@RunWith(AndroidJUnit4::class)
-class RemoveUnusedImagesTest {
+class RemoveUnusedImagesTest : AppCenter() {
@Rule
@JvmField
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/CacheTest.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/CacheTest.kt
new file mode 100644
index 0000000000..299e193c96
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/CacheTest.kt
@@ -0,0 +1,89 @@
+package com.mapbox.mapboxsdk.testapp.offline
+
+import android.content.Context
+import android.support.test.rule.ActivityTestRule
+import android.support.test.runner.AndroidJUnit4
+import com.mapbox.mapboxsdk.offline.OfflineManager
+import com.mapbox.mapboxsdk.testapp.activity.FeatureOverviewActivity
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.CountDownLatch
+
+@RunWith(AndroidJUnit4::class)
+class CacheTest {
+
+ @Rule
+ @JvmField
+ var rule = ActivityTestRule(FeatureOverviewActivity::class.java)
+
+ private val context: Context by lazy { rule.activity }
+
+ private val countDownLatch = CountDownLatch(1)
+
+ @Test
+ fun testSetMaximumAmbientCacheSize() {
+ rule.activity.runOnUiThread {
+ OfflineManager.getInstance(context).setMaximumAmbientCacheSize(10000000, object : OfflineManager.FileSourceCallback {
+ override fun onSuccess() {
+ countDownLatch.countDown()
+ }
+
+ override fun onError(message: String) {
+ Assert.assertNull("onError should not be called", message)
+ }
+ })
+ }
+ countDownLatch.await()
+ }
+
+ @Test
+ fun testSetClearAmbientCache() {
+ rule.activity.runOnUiThread {
+ OfflineManager.getInstance(context).clearAmbientCache(object : OfflineManager.FileSourceCallback {
+ override fun onSuccess() {
+ countDownLatch.countDown()
+ }
+
+ override fun onError(message: String) {
+ Assert.assertNull("onError should not be called", message)
+ }
+ })
+ }
+ countDownLatch.await()
+ }
+
+ @Test
+ fun testSetInvalidateAmbientCache() {
+ rule.activity.runOnUiThread {
+ OfflineManager.getInstance(context).invalidateAmbientCache(object : OfflineManager.FileSourceCallback {
+ override fun onSuccess() {
+ countDownLatch.countDown()
+ }
+
+ override fun onError(message: String) {
+ Assert.assertNull("onError should not be called", message)
+ }
+ })
+ }
+ countDownLatch.await()
+ }
+
+ @Test
+ fun testSetResetDatabase() {
+ rule.activity.runOnUiThread {
+ OfflineManager.getInstance(context).resetDatabase(object : OfflineManager.FileSourceCallback {
+ override fun onSuccess() {
+ countDownLatch.countDown()
+ }
+
+ override fun onError(message: String) {
+ Assert.assertNull("onError should not be called", message)
+ }
+ })
+ }
+ countDownLatch.await()
+ }
+
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/OfflineManagerTest.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/OfflineManagerTest.kt
index 8e5f3f7c5f..6b73623ae7 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/OfflineManagerTest.kt
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/OfflineManagerTest.kt
@@ -3,6 +3,7 @@ package com.mapbox.mapboxsdk.testapp.offline
import android.content.Context
import android.support.test.rule.ActivityTestRule
import android.support.test.runner.AndroidJUnit4
+import com.mapbox.mapboxsdk.AppCenter
import com.mapbox.mapboxsdk.offline.OfflineManager
import com.mapbox.mapboxsdk.offline.OfflineRegion
import com.mapbox.mapboxsdk.storage.FileSource
@@ -18,7 +19,7 @@ import java.util.concurrent.CountDownLatch
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@RunWith(AndroidJUnit4::class)
-class OfflineManagerTest {
+class OfflineManagerTest : AppCenter() {
companion object {
private const val TEST_DB_FILE_NAME = "offline_test.db"
@@ -34,7 +35,7 @@ class OfflineManagerTest {
@Test(timeout = 30_000)
fun a_copyFileFromAssets() {
val latch = CountDownLatch(1)
- rule.runOnUiThread {
+ rule.activity.runOnUiThread {
FileUtils.CopyFileFromAssetsTask(rule.activity, object : FileUtils.OnFileCopiedFromAssetsListener {
override fun onFileCopiedFromAssets() {
latch.countDown()
@@ -51,7 +52,7 @@ class OfflineManagerTest {
@Test(timeout = 30_000)
fun b_mergeRegion() {
val latch = CountDownLatch(1)
- rule.runOnUiThread {
+ rule.activity.runOnUiThread {
OfflineManager.getInstance(context).mergeOfflineRegions(
FileSource.getResourcesCachePath(rule.activity) + "/" + TEST_DB_FILE_NAME,
object : OfflineManager.MergeOfflineRegionsCallback {
@@ -71,7 +72,7 @@ class OfflineManagerTest {
@Test(timeout = 30_000)
fun c_listRegion() {
val latch = CountDownLatch(1)
- rule.runOnUiThread {
+ rule.activity.runOnUiThread {
OfflineManager.getInstance(context).listOfflineRegions(object : OfflineManager.ListOfflineRegionsCallback {
override fun onList(offlineRegions: Array<out OfflineRegion>?) {
assert(offlineRegions?.size == 1)
@@ -88,9 +89,26 @@ class OfflineManagerTest {
}
@Test(timeout = 30_000)
- fun d_deleteRegion() {
+ fun d_invalidateRegion() {
val latch = CountDownLatch(1)
- rule.runOnUiThread {
+ rule.activity.runOnUiThread {
+ mergedRegion.invalidate(object : OfflineRegion.OfflineRegionInvalidateCallback {
+ override fun onInvalidate() {
+ latch.countDown()
+ }
+
+ override fun onError(error: String?) {
+ throw RuntimeException("Unable to delete region")
+ }
+ })
+ }
+ latch.await()
+ }
+
+ @Test(timeout = 30_000)
+ fun e_deleteRegion() {
+ val latch = CountDownLatch(1)
+ rule.activity.runOnUiThread {
mergedRegion.delete(object : OfflineRegion.OfflineRegionDeleteCallback {
override fun onDelete() {
latch.countDown()
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/OfflineUtilsTest.java b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/OfflineUtilsTest.java
index 40fba08c25..48eb8e832d 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/OfflineUtilsTest.java
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/OfflineUtilsTest.java
@@ -1,6 +1,8 @@
package com.mapbox.mapboxsdk.testapp.offline;
import android.support.test.runner.AndroidJUnit4;
+
+import com.mapbox.mapboxsdk.AppCenter;
import com.mapbox.mapboxsdk.testapp.utils.OfflineUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -13,7 +15,7 @@ import static junit.framework.Assert.assertEquals;
import static junit.framework.TestCase.assertTrue;
@RunWith(AndroidJUnit4.class)
-public class OfflineUtilsTest {
+public class OfflineUtilsTest extends AppCenter {
private static final String REGION_NAME = "hello world";
private static final String CONVERTED_REGION_NAME = "{\"FIELD_REGION_NAME\":\"hello world\"}";
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/render/RenderTest.java b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/render/RenderTest.java
index 14b138e917..c74e110b6c 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/render/RenderTest.java
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/render/RenderTest.java
@@ -7,6 +7,8 @@ import android.support.test.espresso.IdlingResourceTimeoutException;
import android.support.test.rule.ActivityTestRule;
import android.support.test.rule.GrantPermissionRule;
import android.support.test.runner.AndroidJUnit4;
+
+import com.mapbox.mapboxsdk.AppCenter;
import com.mapbox.mapboxsdk.testapp.activity.render.RenderTestActivity;
import com.mapbox.mapboxsdk.testapp.utils.SnapshotterIdlingResource;
import org.junit.After;
@@ -28,7 +30,7 @@ import static android.support.test.espresso.matcher.ViewMatchers.withId;
* Instrumentation render tests
*/
@RunWith(AndroidJUnit4.class)
-public class RenderTest {
+public class RenderTest extends AppCenter {
private static final int RENDER_TEST_TIMEOUT = 30;
private SnapshotterIdlingResource idlingResource;
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/storage/FileSourceMapTest.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/storage/FileSourceMapTest.kt
index 5e3489d755..7a5801a814 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/storage/FileSourceMapTest.kt
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/storage/FileSourceMapTest.kt
@@ -3,6 +3,7 @@ package com.mapbox.mapboxsdk.testapp.storage
import android.support.test.annotation.UiThreadTest
import android.support.test.rule.ActivityTestRule
import android.support.test.runner.AndroidJUnit4
+import com.mapbox.mapboxsdk.AppCenter
import com.mapbox.mapboxsdk.storage.FileSource
import com.mapbox.mapboxsdk.testapp.activity.espresso.EspressoTestActivity
import junit.framework.Assert
@@ -15,7 +16,7 @@ import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
@RunWith(AndroidJUnit4::class)
-open class FileSourceMapTest {
+open class FileSourceMapTest : AppCenter() {
private lateinit var fileSourceTestUtils: FileSourceTestUtils
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/storage/FileSourceStandaloneTest.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/storage/FileSourceStandaloneTest.kt
index 08a15b0c54..428d0ed757 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/storage/FileSourceStandaloneTest.kt
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/storage/FileSourceStandaloneTest.kt
@@ -3,6 +3,7 @@ package com.mapbox.mapboxsdk.testapp.storage
import android.support.test.annotation.UiThreadTest
import android.support.test.rule.ActivityTestRule
import android.support.test.runner.AndroidJUnit4
+import com.mapbox.mapboxsdk.AppCenter
import com.mapbox.mapboxsdk.storage.FileSource
import com.mapbox.mapboxsdk.testapp.activity.FeatureOverviewActivity
import org.junit.*
@@ -12,7 +13,7 @@ import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class)
-class FileSourceStandaloneTest {
+class FileSourceStandaloneTest : AppCenter() {
private lateinit var fileSourceTestUtils: FileSourceTestUtils
private lateinit var fileSource: FileSource
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/storage/FileSourceTestUtils.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/storage/FileSourceTestUtils.kt
index c79d3b2752..0010880414 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/storage/FileSourceTestUtils.kt
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/storage/FileSourceTestUtils.kt
@@ -2,12 +2,13 @@ package com.mapbox.mapboxsdk.testapp.storage
import android.app.Activity
import android.support.annotation.WorkerThread
+import com.mapbox.mapboxsdk.AppCenter
import com.mapbox.mapboxsdk.storage.FileSource
import junit.framework.Assert
import java.io.File
import java.util.concurrent.CountDownLatch
-class FileSourceTestUtils(private val activity: Activity) {
+class FileSourceTestUtils(private val activity: Activity) : AppCenter() {
val originalPath = FileSource.getResourcesCachePath(activity)
val testPath = "$originalPath/test"
val testPath2 = "$originalPath/test2"
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml
index bb2bef35fb..d2a5032c81 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml
@@ -709,6 +709,17 @@
android:value=".activity.FeatureOverviewActivity" />
</activity>
<activity
+ android:name=".activity.storage.CacheManagementActivity"
+ android:description="@string/description_cache_management"
+ android:label="@string/activity_cache_management">
+ <meta-data
+ android:name="@string/category"
+ android:value="@string/category_storage" />
+ <meta-data
+ android:name="android.support.PARENT_ACTIVITY"
+ android:value=".activity.FeatureOverviewActivity" />
+ </activity>
+ <activity
android:name=".activity.maplayout.BottomSheetActivity"
android:description="@string/description_bottom_sheet"
android:label="@string/activity_bottom_sheet">
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/assets/points-sf.geojson b/platform/android/MapboxGLAndroidSDKTestApp/src/main/assets/points-sf.geojson
new file mode 100644
index 0000000000..9c96c4ef9f
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/assets/points-sf.geojson
@@ -0,0 +1,115 @@
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 237.5580596923828,
+ 37.78563944612241
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 237.58346557617185,
+ 37.743571187449064
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 237.51068115234375,
+ 37.72347854862523
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 237.51583099365234,
+ 37.77125750792944
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 237.5913619995117,
+ 37.795406713958236
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 237.56080627441403,
+ 37.69441603823106
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 237.62981414794922,
+ 37.722392304715825
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 237.52235412597656,
+ 37.807071480609274
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 237.53608703613278,
+ 37.75334401310656
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 237.57591247558594,
+ 37.77071473849609
+ ]
+ }
+ }
+ ]
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/LatLngBoundsActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/LatLngBoundsActivity.java
deleted file mode 100644
index 6d73ee776e..0000000000
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/LatLngBoundsActivity.java
+++ /dev/null
@@ -1,172 +0,0 @@
-package com.mapbox.mapboxsdk.testapp.activity.camera;
-
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.design.widget.BottomSheetBehavior;
-import android.support.v7.app.AppCompatActivity;
-import android.view.View;
-
-import com.mapbox.mapboxsdk.annotations.MarkerOptions;
-import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
-import com.mapbox.mapboxsdk.geometry.LatLng;
-import com.mapbox.mapboxsdk.geometry.LatLngBounds;
-import com.mapbox.mapboxsdk.maps.MapView;
-import com.mapbox.mapboxsdk.maps.MapboxMap;
-import com.mapbox.mapboxsdk.maps.Style;
-import com.mapbox.mapboxsdk.testapp.R;
-import com.mapbox.mapboxsdk.testapp.view.LockableBottomSheetBehavior;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Test activity showcasing using the LatLngBounds camera API.
- */
-public class LatLngBoundsActivity extends AppCompatActivity implements View.OnClickListener {
-
- private static final List<LatLng> LOCATIONS = new ArrayList<LatLng>() {
- {
- add(new LatLng(37.806866, -122.422502));
- add(new LatLng(37.812905, -122.477605));
- add(new LatLng(37.826944, -122.423188));
- add(new LatLng(37.752676, -122.447736));
- add(new LatLng(37.769305, -122.479322));
- add(new LatLng(37.749834, -122.417867));
- add(new LatLng(37.756149, -122.405679));
- add(new LatLng(37.751403, -122.387397));
- add(new LatLng(37.793064, -122.391517));
- add(new LatLng(37.769122, -122.427394));
- }
- };
- private static final LatLngBounds BOUNDS = new LatLngBounds.Builder().includes(LOCATIONS).build();
- private static final int ANIMATION_DURATION_LONG = 450;
- private static final int ANIMATION_DURATION_SHORT = 250;
- private static final int BOUNDS_PADDING_DIVIDER_SMALL = 3;
- private static final int BOUNDS_PADDING_DIVIDER_LARGE = 9;
-
- private MapView mapView;
- private MapboxMap mapboxMap;
- private View bottomSheet;
- private LockableBottomSheetBehavior bottomSheetBehavior;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_latlngbounds);
- initBottomSheet();
- initMapView(savedInstanceState);
- }
-
- private void initMapView(Bundle savedInstanceState) {
- mapView = findViewById(R.id.mapView);
- mapView.onCreate(savedInstanceState);
- mapView.getMapAsync(map -> {
- mapboxMap = map;
- disableGestures();
- moveToBounds(bottomSheet.getMeasuredHeight(), BOUNDS_PADDING_DIVIDER_SMALL, ANIMATION_DURATION_SHORT);
- loadStyle();
- });
- }
-
- private void loadStyle() {
- mapboxMap.setStyle(new Style.Builder().fromUri(Style.MAPBOX_STREETS), style -> {
- addMarkers();
- initFab();
- });
- }
-
- private void disableGestures() {
- mapboxMap.getUiSettings().setTiltGesturesEnabled(false);
- mapboxMap.getUiSettings().setRotateGesturesEnabled(false);
- }
-
- private void addMarkers() {
- for (LatLng location : LOCATIONS) {
- mapboxMap.addMarker(new MarkerOptions().position(location));
- }
- }
-
- private void initFab() {
- findViewById(R.id.fab).setOnClickListener(this);
- }
-
- @Override
- public void onClick(View v) {
- bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
- v.animate().alpha(0.0f).setDuration(ANIMATION_DURATION_SHORT);
- }
-
- private void initBottomSheet() {
- bottomSheet = findViewById(R.id.bottom_sheet);
- bottomSheetBehavior = (LockableBottomSheetBehavior) BottomSheetBehavior.from(bottomSheet);
- bottomSheetBehavior.setLocked(true);
- bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
- @Override
- public void onStateChanged(@NonNull View bottomSheet, int newState) {
- if (newState == BottomSheetBehavior.STATE_SETTLING && mapboxMap != null) {
- moveToBounds(0, BOUNDS_PADDING_DIVIDER_LARGE, ANIMATION_DURATION_LONG);
- }
- }
-
- @Override
- public void onSlide(@NonNull View bottomSheet, float slideOffset) {
-
- }
- });
- }
-
- private void moveToBounds(int verticalOffset, int boundsPaddingDivider, int duration) {
- int paddingHorizontal = mapView.getMeasuredWidth() / boundsPaddingDivider;
- int paddingVertical = (mapView.getMeasuredHeight() - verticalOffset) / boundsPaddingDivider;
- mapboxMap.animateCamera(CameraUpdateFactory.newLatLngBounds(
- BOUNDS,
- paddingHorizontal,
- paddingVertical,
- paddingHorizontal,
- paddingVertical + verticalOffset),
- duration
- );
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- mapView.onStart();
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- mapView.onResume();
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- mapView.onPause();
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- mapView.onStop();
- }
-
- @Override
- public void onLowMemory() {
- super.onLowMemory();
- mapView.onLowMemory();
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- mapView.onDestroy();
- }
-
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- mapView.onSaveInstanceState(outState);
- }
-}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/LatLngBoundsActivity.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/LatLngBoundsActivity.kt
new file mode 100644
index 0000000000..6f318f77eb
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/LatLngBoundsActivity.kt
@@ -0,0 +1,167 @@
+package com.mapbox.mapboxsdk.testapp.activity.camera
+
+import android.content.Context
+import android.os.Bundle
+import android.support.design.widget.BottomSheetBehavior
+import android.support.v7.app.AppCompatActivity
+import android.view.View
+import com.mapbox.geojson.FeatureCollection
+import com.mapbox.geojson.FeatureCollection.fromJson
+import com.mapbox.geojson.Point
+import com.mapbox.mapboxsdk.geometry.LatLng
+import com.mapbox.mapboxsdk.geometry.LatLngBounds
+import com.mapbox.mapboxsdk.maps.MapboxMap
+import com.mapbox.mapboxsdk.maps.Style
+import com.mapbox.mapboxsdk.style.layers.Property.ICON_ANCHOR_CENTER
+import com.mapbox.mapboxsdk.style.layers.PropertyFactory.*
+import com.mapbox.mapboxsdk.style.layers.SymbolLayer
+import com.mapbox.mapboxsdk.style.sources.GeoJsonSource
+import com.mapbox.mapboxsdk.testapp.R
+import com.mapbox.mapboxsdk.testapp.utils.GeoParseUtil.loadStringFromAssets
+import com.mapbox.mapboxsdk.utils.BitmapUtils
+import kotlinx.android.synthetic.main.activity_latlngbounds.*
+import java.net.URISyntaxException
+
+/**
+ * Test activity showcasing using the LatLngBounds camera API.
+ */
+class LatLngBoundsActivity : AppCompatActivity() {
+
+ private lateinit var mapboxMap: MapboxMap
+ private lateinit var bottomSheetBehavior: BottomSheetBehavior<*>
+ private lateinit var bounds: LatLngBounds
+
+ private val peekHeight by lazy {
+ 375.toPx(this) //375dp
+ }
+
+ private val additionalPadding by lazy {
+ 32.toPx(this) //32dp
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_latlngbounds)
+ initMapView(savedInstanceState)
+ }
+
+ private fun initMapView(savedInstanceState: Bundle?) {
+ mapView.onCreate(savedInstanceState)
+ mapView.getMapAsync { map ->
+ mapboxMap = map
+
+ val featureCollection: FeatureCollection = fromJson(loadStringFromAssets(this, "points-sf.geojson"))
+ bounds = createBounds(featureCollection)
+
+ map.getCameraForLatLngBounds(bounds, createPadding(peekHeight))?.let {
+ map.cameraPosition = it
+ }
+
+ try {
+ loadStyle(featureCollection)
+ } catch (e: URISyntaxException) {
+ e.printStackTrace()
+ }
+ }
+ }
+
+ private fun loadStyle(featureCollection: FeatureCollection) {
+ mapboxMap.setStyle(Style.Builder()
+ .fromUri(Style.MAPBOX_STREETS)
+ .withLayer(SymbolLayer("symbol", "symbol")
+ .withProperties(
+ iconAllowOverlap(true),
+ iconIgnorePlacement(true),
+ iconImage("icon"),
+ iconAnchor(ICON_ANCHOR_CENTER)
+ )
+ )
+ .withSource(GeoJsonSource("symbol", featureCollection))
+ .withImage("icon", BitmapUtils.getDrawableFromRes(this@LatLngBoundsActivity, R.drawable.ic_android)!!)
+ ) {
+ initBottomSheet()
+ fab.setOnClickListener { bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED }
+ }
+ }
+
+ private fun initBottomSheet() {
+ bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet)
+ bottomSheetBehavior.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
+ override fun onSlide(bottomSheet: View, slideOffset: Float) {
+ val offset = convertSlideOffset(slideOffset)
+ val bottomPadding = (peekHeight * offset).toInt()
+
+ mapboxMap.getCameraForLatLngBounds(bounds, createPadding(bottomPadding))?.let {
+ mapboxMap.cameraPosition = it
+ }
+ }
+
+ override fun onStateChanged(bottomSheet: View, newState: Int) {
+ // no-op
+ }
+ })
+ }
+
+ // slideOffset ranges from NaN to -1.0, range from 1.0 to 0 instead
+ fun convertSlideOffset(slideOffset: Float): Float {
+ return if (slideOffset.equals(Float.NaN)) {
+ 1.0f
+ } else {
+ 1 + slideOffset
+ }
+ }
+
+ fun createPadding(bottomPadding: Int): IntArray {
+ return intArrayOf(additionalPadding, additionalPadding, additionalPadding, bottomPadding)
+ }
+
+ private fun createBounds(featureCollection: FeatureCollection): LatLngBounds {
+ val boundsBuilder = LatLngBounds.Builder()
+ featureCollection.features()?.let {
+ for (feature in it) {
+ val point = feature.geometry() as Point
+ boundsBuilder.include(LatLng(point.latitude(), point.longitude()))
+ }
+ }
+ return boundsBuilder.build()
+ }
+
+ override fun onStart() {
+ super.onStart()
+ mapView.onStart()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ mapView.onResume()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ mapView.onPause()
+ }
+
+ override fun onStop() {
+ super.onStop()
+ mapView.onStop()
+ }
+
+ override fun onLowMemory() {
+ super.onLowMemory()
+ mapView.onLowMemory()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ mapView.onDestroy()
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ mapView.onSaveInstanceState(outState)
+ }
+
+}
+
+fun Int.toPx(context: Context): Int = (this * context.resources.displayMetrics.density).toInt()
+
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/LocationModesActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/LocationModesActivity.java
index f9adf608a1..6ec64e1d0c 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/LocationModesActivity.java
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/LocationModesActivity.java
@@ -356,6 +356,8 @@ public class LocationModesActivity extends AppCompatActivity implements OnMapRea
private void showTrackingListDialog() {
List<String> trackingTypes = new ArrayList<>();
trackingTypes.add("None");
+ trackingTypes.add("None Compass");
+ trackingTypes.add("None GPS");
trackingTypes.add("Tracking");
trackingTypes.add("Tracking Compass");
trackingTypes.add("Tracking GPS");
@@ -370,6 +372,10 @@ public class LocationModesActivity extends AppCompatActivity implements OnMapRea
locationTrackingBtn.setText(selectedTrackingType);
if (selectedTrackingType.contentEquals("None")) {
setCameraTrackingMode(CameraMode.NONE);
+ } else if (selectedTrackingType.contentEquals("None Compass")) {
+ setCameraTrackingMode(CameraMode.NONE_COMPASS);
+ } else if (selectedTrackingType.contentEquals("None GPS")) {
+ setCameraTrackingMode(CameraMode.NONE_GPS);
} else if (selectedTrackingType.contentEquals("Tracking")) {
setCameraTrackingMode(CameraMode.TRACKING);
} else if (selectedTrackingType.contentEquals("Tracking Compass")) {
@@ -409,6 +415,10 @@ public class LocationModesActivity extends AppCompatActivity implements OnMapRea
this.cameraMode = currentMode;
if (currentMode == CameraMode.NONE) {
locationTrackingBtn.setText("None");
+ } else if (currentMode == CameraMode.NONE_COMPASS) {
+ locationTrackingBtn.setText("None Compass");
+ } else if (currentMode == CameraMode.NONE_GPS) {
+ locationTrackingBtn.setText("None GPS");
} else if (currentMode == CameraMode.TRACKING) {
locationTrackingBtn.setText("Tracking");
} else if (currentMode == CameraMode.TRACKING_COMPASS) {
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/DeleteRegionActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/DeleteRegionActivity.java
index 037c51f723..11ee68702f 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/DeleteRegionActivity.java
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/DeleteRegionActivity.java
@@ -12,7 +12,6 @@ import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
-
import com.mapbox.mapboxsdk.offline.OfflineManager;
import com.mapbox.mapboxsdk.offline.OfflineRegion;
import com.mapbox.mapboxsdk.testapp.R;
@@ -25,7 +24,8 @@ import java.util.List;
/**
* Test activity showing integration of deleting an OfflineRegion.
*/
-public class DeleteRegionActivity extends AppCompatActivity implements AdapterView.OnItemClickListener {
+public class DeleteRegionActivity extends AppCompatActivity implements AdapterView.OnItemClickListener,
+ AdapterView.OnItemLongClickListener {
private OfflineRegionAdapter adapter;
@@ -34,10 +34,11 @@ public class DeleteRegionActivity extends AppCompatActivity implements AdapterVi
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_offline_region_delete);
- ListView listView = (ListView) findViewById(R.id.listView);
+ ListView listView = findViewById(R.id.listView);
listView.setAdapter(adapter = new OfflineRegionAdapter(this));
listView.setEmptyView(findViewById(android.R.id.empty));
listView.setOnItemClickListener(this);
+ listView.setOnItemLongClickListener(this);
}
@Override
@@ -58,6 +59,23 @@ public class DeleteRegionActivity extends AppCompatActivity implements AdapterVi
builder.show();
}
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
+ final OfflineRegion region = adapter.getItem(position);
+ region.invalidate(new OfflineRegion.OfflineRegionInvalidateCallback() {
+ @Override
+ public void onInvalidate() {
+ Toast.makeText(DeleteRegionActivity.this, "Invalidate region success", Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ public void onError(String error) {
+ Toast.makeText(DeleteRegionActivity.this, "Error:" + error, Toast.LENGTH_LONG).show();
+ }
+ });
+ return true;
+ }
+
private void delete(OfflineRegion region) {
region.delete(new OfflineRegion.OfflineRegionDeleteCallback() {
@Override
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/storage/CacheManagementActivity.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/storage/CacheManagementActivity.kt
new file mode 100644
index 0000000000..c9fdb79e6e
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/storage/CacheManagementActivity.kt
@@ -0,0 +1,79 @@
+package com.mapbox.mapboxsdk.testapp.activity.storage
+
+import android.os.Bundle
+import android.os.Looper
+import android.support.design.widget.Snackbar
+import android.support.v7.app.AppCompatActivity
+import com.mapbox.mapboxsdk.offline.OfflineManager
+import com.mapbox.mapboxsdk.storage.FileSource
+import com.mapbox.mapboxsdk.testapp.R
+import junit.framework.Assert.assertTrue
+import kotlinx.android.synthetic.main.activity_cache_management.*
+
+/**
+ * Test activity showcasing the cache management APIs
+ */
+class CacheManagementActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_cache_management)
+
+ val fileSource = OfflineManager.getInstance(this)
+ resetDatabaseButton.setOnClickListener {
+ fileSource.resetDatabase(object : OfflineManager.FileSourceCallback {
+ override fun onSuccess() {
+ showSnackbar("Reset database success")
+ }
+
+ override fun onError(message: String) {
+ showSnackbar("Reset database fail: $message")
+ }
+ })
+ }
+
+ invalidateAmbientCacheButton.setOnClickListener {
+ fileSource.invalidateAmbientCache(object : OfflineManager.FileSourceCallback {
+ override fun onSuccess() {
+ showSnackbar("Invalidate ambient cache success")
+ }
+
+ override fun onError(message: String) {
+ showSnackbar("Invalidate ambient cache fail: $message")
+ }
+ })
+ }
+
+ clearAmbientCacheButton.setOnClickListener {
+ fileSource.clearAmbientCache(object : OfflineManager.FileSourceCallback {
+ override fun onSuccess() {
+ showSnackbar("Clear ambient cache success")
+ }
+
+ override fun onError(message: String) {
+ showSnackbar("Clear ambient cache fail: $message")
+ }
+ })
+ }
+
+ setMaximumAmbientCacheSizeButton.setOnClickListener {
+ fileSource.setMaximumAmbientCacheSize(5000000, object : OfflineManager.FileSourceCallback {
+ override fun onSuccess() {
+ showSnackbar("Set maximum ambient cache size success")
+ }
+
+ override fun onError(message: String) {
+ showSnackbar("Set maximum ambient cache size fail: $message")
+ }
+ })
+ }
+ }
+
+ fun showSnackbar(message: String) {
+ // validate that all callbacks occur on main thread
+ assertTrue(Looper.myLooper() == Looper.getMainLooper())
+
+ // show snackbar
+ Snackbar.make(container, message, Snackbar.LENGTH_SHORT).show()
+ }
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/SymbolLayerActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/SymbolLayerActivity.java
index 243fa67ec4..689ce1f0a7 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/SymbolLayerActivity.java
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/SymbolLayerActivity.java
@@ -43,7 +43,9 @@ import static com.mapbox.mapboxsdk.style.expressions.Expression.format;
import static com.mapbox.mapboxsdk.style.expressions.Expression.formatEntry;
import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
import static com.mapbox.mapboxsdk.style.expressions.Expression.literal;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.match;
import static com.mapbox.mapboxsdk.style.expressions.Expression.rgb;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.stop;
import static com.mapbox.mapboxsdk.style.expressions.Expression.switchCase;
import static com.mapbox.mapboxsdk.style.expressions.Expression.toBool;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAllowOverlap;
@@ -51,6 +53,7 @@ import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAnchor;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconColor;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconIgnorePlacement;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconOpacity;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconSize;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textAllowOverlap;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textAnchor;
@@ -198,6 +201,12 @@ public class SymbolLayerActivity extends AppCompatActivity implements MapboxMap.
// use DDS
boolean selected = feature.getBooleanProperty(SELECTED_FEATURE_PROPERTY);
feature.addBooleanProperty(SELECTED_FEATURE_PROPERTY, !selected);
+
+ // validate symbol flicker regression for #13407
+ markerSymbolLayer.setProperties(iconOpacity(match(
+ get(ID_FEATURE_PROPERTY), literal(1.0f),
+ stop(feature.getStringProperty("id"), selected ? 0.3f : 1.0f)
+ )));
}
}
markerSource.setGeoJson(markerCollection);
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_arrow_upward.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_arrow_upward.xml
new file mode 100644
index 0000000000..c64ae35166
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_arrow_upward.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M4,12l1.41,1.41L11,7.83V20h2V7.83l5.58,5.59L20,12l-8,-8 -8,8z"/>
+</vector>
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_cache_management.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_cache_management.xml
new file mode 100644
index 0000000000..a79ed9352b
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_cache_management.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/container"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <Button
+ android:id="@+id/resetDatabaseButton"
+ android:text="Reset Database"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <Button
+ android:id="@+id/invalidateAmbientCacheButton"
+ android:text="Invalidate Ambient Cache"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <Button
+ android:id="@+id/clearAmbientCacheButton"
+ android:text="Clear Ambient Cache"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <Button
+ android:id="@+id/setMaximumAmbientCacheSizeButton"
+ android:text="Set Maximum Ambient Cache Size to 5mb"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_latlngbounds.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_latlngbounds.xml
index e565c3c9d1..e25fd1882c 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_latlngbounds.xml
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_latlngbounds.xml
@@ -1,36 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/coordinator_layout"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:fitsSystemWindows="true"
- android:orientation="vertical">
-
- <com.mapbox.mapboxsdk.maps.MapView
- android:id="@id/mapView"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
- app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
+ android:fitsSystemWindows="true"
+ android:orientation="vertical">
+
+ <com.mapbox.mapboxsdk.maps.MapView
+ android:id="@id/mapView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<android.support.v4.widget.NestedScrollView
- android:id="@+id/bottom_sheet"
- android:layout_width="match_parent"
- android:layout_height="375dp"
- android:background="@color/primaryDark"
- app:behavior_hideable="true"
- app:behavior_skipCollapsed="false"
- app:layout_behavior="com.mapbox.mapboxsdk.testapp.view.LockableBottomSheetBehavior"/>
+ android:id="@+id/bottomSheet"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/primaryDark"
+ app:behavior_hideable="true"
+ app:behavior_peekHeight="375dp"
+ app:layout_behavior="android.support.design.widget.BottomSheetBehavior">
+
+ <TextView
+ android:text="Hello World"
+ android:gravity="center"
+ android:textSize="120sp"
+ android:textColor="@android:color/white"
+ android:background="@color/primary"
+ android:layout_width="match_parent"
+ android:layout_height="375dp"/>
+
+ </android.support.v4.widget.NestedScrollView>
<android.support.design.widget.FloatingActionButton
- android:id="@+id/fab"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_margin="@dimen/fab_margin"
- android:src="@drawable/ic_arrow_downward"
- app:backgroundTint="@color/primary"
- app:layout_anchor="@id/bottom_sheet"
- app:layout_anchorGravity="top|end"/>
+ android:id="@+id/fab"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/fab_margin"
+ android:src="@drawable/ic_arrow_upward"
+ app:backgroundTint="@color/primary"
+ app:layout_anchor="@id/bottomSheet"
+ app:layout_anchorGravity="top|end"/>
</android.support.design.widget.CoordinatorLayout> \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml
index 684220f2c4..082eb39256 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml
@@ -51,6 +51,7 @@
<string name="description_map_in_dialog">Display a map inside a dialog fragment</string>
<string name="description_circle_layer">Show bus stops and route in Singapore</string>
<string name="description_url_transform">Transform urls on the fly</string>
+ <string name="description_cache_management">Control the cache management with FileSource API</string>
<string name="description_restricted_bounds">Limit viewport to Iceland</string>
<string name="description_fill_extrusion_layer">Shows how to add 3D extruded shapes</string>
<string name="description_building_fill_extrusion_layer">Shows how to show 3D extruded buildings</string>
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml
index 027198c71b..94566ea995 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml
@@ -50,6 +50,7 @@
<string name="activity_map_visibility">Visibility Map</string>
<string name="activity_map_in_dialog">Dialog with map</string>
<string name="activity_url_transform">Url transform</string>
+ <string name="activity_cache_management">Cache management</string>
<string name="activity_restricted_bounds">Restrict camera to a bounds</string>
<string name="activity_fill_extrusion_layer">Fill extrusions</string>
<string name="activity_building_fill_extrusion_layer">Building layer</string>
diff --git a/platform/android/core-files.json b/platform/android/core-files.json
index 7e2f7cc07b..62ecc0c671 100644
--- a/platform/android/core-files.json
+++ b/platform/android/core-files.json
@@ -81,6 +81,7 @@
"platform/android/src/style/value.cpp",
"platform/android/src/text/collator.cpp",
"platform/android/src/text/local_glyph_rasterizer.cpp",
+ "platform/android/src/text/format_number.cpp",
"platform/android/src/gl_functions.cpp",
"platform/android/src/thread.cpp",
"platform/android/src/timer.cpp",
diff --git a/platform/android/gradle/dependencies.gradle b/platform/android/gradle/dependencies.gradle
index b7e5298477..a7ff1dbfe6 100644
--- a/platform/android/gradle/dependencies.gradle
+++ b/platform/android/gradle/dependencies.gradle
@@ -32,7 +32,8 @@ ext {
lint : '26.1.4',
gms : '16.0.0',
soLoader : '0.6.0',
- jacoco : '0.8.3'
+ jacoco : '0.8.3',
+ appcenter : '1.4'
]
dependenciesList = [
@@ -53,6 +54,7 @@ ext {
testEspressoIntents : "com.android.support.test.espresso:espresso-intents:${versions.espresso}",
testEspressoContrib : "com.android.support.test.espresso:espresso-contrib:${versions.espresso}",
testUiAutomator : "com.android.support.test.uiautomator:uiautomator-v18:${versions.uiAutomator}",
+ appCenter : "com.microsoft.appcenter:espresso-test-extension:${versions.appcenter}",
commonsIO : 'commons-io:commons-io:2.5',
supportAnnotations : "com.android.support:support-annotations:${versions.supportLib}",
supportAppcompatV7 : "com.android.support:appcompat-v7:${versions.supportLib}",
diff --git a/platform/android/scripts/exclude-activity-gen.json b/platform/android/scripts/exclude-activity-gen.json
index f6156eb0ea..0f5bb81b55 100644
--- a/platform/android/scripts/exclude-activity-gen.json
+++ b/platform/android/scripts/exclude-activity-gen.json
@@ -51,5 +51,6 @@
"FragmentBackStackActivity",
"ChildFragmentMapInDialogActivity",
"PerformanceMeasurementActivity",
- "DownloadRegionActivity"
+ "DownloadRegionActivity",
+ "CacheManagementActivity"
]
diff --git a/platform/android/src/jni.cpp b/platform/android/src/jni.cpp
index 410c962384..088b3b796c 100755
--- a/platform/android/src/jni.cpp
+++ b/platform/android/src/jni.cpp
@@ -54,6 +54,7 @@
#endif
#include "text/collator_jni.hpp"
#include "text/local_glyph_rasterizer_jni.hpp"
+#include "text/format_number_jni.hpp"
#include "logger.hpp"
namespace mbgl {
@@ -200,6 +201,7 @@ void registerNatives(JavaVM *vm) {
Locale::registerNative(env);
Collator::registerNative(env);
StringUtils::registerNative(env);
+ NumberFormat::registerNative(env);
// Logger
Logger::registerNative(env);
diff --git a/platform/android/src/native_map_view.cpp b/platform/android/src/native_map_view.cpp
index 48b65c4c02..47f2c6d9aa 100755
--- a/platform/android/src/native_map_view.cpp
+++ b/platform/android/src/native_map_view.cpp
@@ -516,6 +516,8 @@ void NativeMapView::setVisibleCoordinateBounds(JNIEnv& env, const jni::Array<jni
void NativeMapView::setContentPadding(JNIEnv&, float top, float left, float bottom, float right) {
insets = {top, left, bottom, right};
+ // invalidate the camera position to consider the new padding
+ map->jumpTo(map->getCameraOptions(insets));
}
jni::Local<jni::Array<jni::jfloat>> NativeMapView::getContentPadding(JNIEnv& env) {
diff --git a/platform/android/src/offline/offline_manager.cpp b/platform/android/src/offline/offline_manager.cpp
index 54b1142845..029252f786 100644
--- a/platform/android/src/offline/offline_manager.cpp
+++ b/platform/android/src/offline/offline_manager.cpp
@@ -104,10 +104,104 @@ void OfflineManager::mergeOfflineRegions(jni::JNIEnv& env_, const jni::Object<Fi
});
}
+void OfflineManager::resetDatabase(jni::JNIEnv& env_, const jni::Object<FileSourceCallback>& callback_) {
+ auto globalCallback = jni::NewGlobal<jni::EnvAttachingDeleter>(env_, callback_);
+
+ fileSource->resetDatabase([
+ //Keep a shared ptr to a global reference of the callback so they are not GC'd in the meanwhile
+ callback = std::make_shared<decltype(globalCallback)>(std::move(globalCallback))
+ ](std::exception_ptr exception) mutable {
+
+ // Reattach, the callback comes from a different thread
+ android::UniqueEnv env = android::AttachEnv();
+
+ if (exception) {
+ OfflineManager::FileSourceCallback::onError(*env, *callback, jni::Make<jni::String>(*env, mbgl::util::toString(exception)));
+ } else {
+ OfflineManager::FileSourceCallback::onSuccess(*env, *callback);
+ }
+ });
+}
+
+void OfflineManager::invalidateAmbientCache(jni::JNIEnv& env_, const jni::Object<FileSourceCallback>& callback_) {
+ auto globalCallback = jni::NewGlobal<jni::EnvAttachingDeleter>(env_, callback_);
+
+ fileSource->invalidateAmbientCache([
+ //Keep a shared ptr to a global reference of the callback so they are not GC'd in the meanwhile
+ callback = std::make_shared<decltype(globalCallback)>(std::move(globalCallback))
+ ](std::exception_ptr exception) mutable {
+
+ // Reattach, the callback comes from a different thread
+ android::UniqueEnv env = android::AttachEnv();
+
+ if (exception) {
+ OfflineManager::FileSourceCallback::onError(*env, *callback, jni::Make<jni::String>(*env, mbgl::util::toString(exception)));
+ } else {
+ OfflineManager::FileSourceCallback::onSuccess(*env, *callback);
+ }
+ });
+}
+
+void OfflineManager::clearAmbientCache(jni::JNIEnv& env_, const jni::Object<FileSourceCallback>& callback_) {
+ auto globalCallback = jni::NewGlobal<jni::EnvAttachingDeleter>(env_, callback_);
+
+ fileSource->clearAmbientCache([
+ //Keep a shared ptr to a global reference of the callback so they are not GC'd in the meanwhile
+ callback = std::make_shared<decltype(globalCallback)>(std::move(globalCallback))
+ ](std::exception_ptr exception) mutable {
+
+ // Reattach, the callback comes from a different thread
+ android::UniqueEnv env = android::AttachEnv();
+
+ if (exception) {
+ OfflineManager::FileSourceCallback::onError(*env, *callback, jni::Make<jni::String>(*env, mbgl::util::toString(exception)));
+ } else {
+ OfflineManager::FileSourceCallback::onSuccess(*env, *callback);
+ }
+ });
+}
+
+void OfflineManager::setMaximumAmbientCacheSize(jni::JNIEnv& env_, const jni::jlong size_, const jni::Object<FileSourceCallback>& callback_) {
+ auto globalCallback = jni::NewGlobal<jni::EnvAttachingDeleter>(env_, callback_);
+
+ fileSource->setMaximumAmbientCacheSize(size_, [
+ //Keep a shared ptr to a global reference of the callback so they are not GC'd in the meanwhile
+ callback = std::make_shared<decltype(globalCallback)>(std::move(globalCallback))
+ ](std::exception_ptr exception) mutable {
+
+ // Reattach, the callback comes from a different thread
+ android::UniqueEnv env = android::AttachEnv();
+
+ if (exception) {
+ OfflineManager::FileSourceCallback::onError(*env, *callback, jni::Make<jni::String>(*env, mbgl::util::toString(exception)));
+ } else {
+ OfflineManager::FileSourceCallback::onSuccess(*env, *callback);
+ }
+ });
+}
+
+// FileSource::FileSourceCallback //
+
+void OfflineManager::FileSourceCallback::onSuccess(jni::JNIEnv& env,
+ const jni::Object<OfflineManager::FileSourceCallback>& callback) {
+ static auto& javaClass = jni::Class<OfflineManager::FileSourceCallback>::Singleton(env);
+ static auto method = javaClass.GetMethod<void ()>(env, "onSuccess");
+ callback.Call(env, method);
+}
+
+void OfflineManager::FileSourceCallback::onError(jni::JNIEnv& env,
+ const jni::Object<OfflineManager::FileSourceCallback>& callback,
+ const jni::String& message) {
+ static auto& javaClass = jni::Class<OfflineManager::FileSourceCallback>::Singleton(env);
+ static auto method = javaClass.GetMethod<void (jni::String)>(env, "onError");
+ callback.Call(env, method, message);
+}
+
void OfflineManager::registerNative(jni::JNIEnv& env) {
jni::Class<ListOfflineRegionsCallback>::Singleton(env);
jni::Class<CreateOfflineRegionCallback>::Singleton(env);
jni::Class<MergeOfflineRegionsCallback>::Singleton(env);
+ jni::Class<FileSourceCallback>::Singleton(env);
static auto& javaClass = jni::Class<OfflineManager>::Singleton(env);
@@ -121,6 +215,10 @@ void OfflineManager::registerNative(jni::JNIEnv& env) {
METHOD(&OfflineManager::listOfflineRegions, "listOfflineRegions"),
METHOD(&OfflineManager::createOfflineRegion, "createOfflineRegion"),
METHOD(&OfflineManager::mergeOfflineRegions, "mergeOfflineRegions"),
+ METHOD(&OfflineManager::resetDatabase, "nativeResetDatabase"),
+ METHOD(&OfflineManager::invalidateAmbientCache, "nativeInvalidateAmbientCache"),
+ METHOD(&OfflineManager::clearAmbientCache, "nativeClearAmbientCache"),
+ METHOD(&OfflineManager::setMaximumAmbientCacheSize, "nativeSetMaximumAmbientCacheSize"),
METHOD(&OfflineManager::putResourceWithUrl, "putResourceWithUrl"));
}
diff --git a/platform/android/src/offline/offline_manager.hpp b/platform/android/src/offline/offline_manager.hpp
index d0b637b900..058cfb5b48 100644
--- a/platform/android/src/offline/offline_manager.hpp
+++ b/platform/android/src/offline/offline_manager.hpp
@@ -55,6 +55,14 @@ public:
mbgl::OfflineRegions&);
};
+ struct FileSourceCallback {
+ static constexpr auto Name() { return "com/mapbox/mapboxsdk/offline/OfflineManager$FileSourceCallback";}
+
+ static void onSuccess(jni::JNIEnv&, const jni::Object<OfflineManager::FileSourceCallback>&);
+
+ static void onError(jni::JNIEnv&, const jni::Object<OfflineManager::FileSourceCallback>&, const jni::String&);
+ };
+
static constexpr auto Name() { return "com/mapbox/mapboxsdk/offline/OfflineManager"; };
static void registerNative(jni::JNIEnv&);
@@ -85,6 +93,13 @@ public:
const jni::String& eTag,
jboolean mustRevalidate);
+ void resetDatabase(jni::JNIEnv&, const jni::Object<FileSourceCallback>& callback_);
+
+ void invalidateAmbientCache(jni::JNIEnv&, const jni::Object<FileSourceCallback>& callback_);
+
+ void clearAmbientCache(jni::JNIEnv&, const jni::Object<FileSourceCallback>& callback_);
+
+ void setMaximumAmbientCacheSize(jni::JNIEnv&, const jni::jlong size, const jni::Object<FileSourceCallback>& callback_);
private:
std::shared_ptr<mbgl::DefaultFileSource> fileSource;
diff --git a/platform/android/src/offline/offline_region.cpp b/platform/android/src/offline/offline_region.cpp
index e0f28631b4..ac9f491ab6 100644
--- a/platform/android/src/offline/offline_region.cpp
+++ b/platform/android/src/offline/offline_region.cpp
@@ -119,6 +119,24 @@ void OfflineRegion::deleteOfflineRegion(jni::JNIEnv& env_, const jni::Object<Off
});
}
+void OfflineRegion::invalidateOfflineRegion(jni::JNIEnv& env_, const jni::Object<OfflineRegionInvalidateCallback>& callback_) {
+ auto globalCallback = jni::NewGlobal<jni::EnvAttachingDeleter>(env_, callback_);
+
+ fileSource->invalidateOfflineRegion(*region, [
+ //Ensure the object is not gc'd in the meanwhile
+ callback = std::make_shared<decltype(globalCallback)>(std::move(globalCallback))
+ ](std::exception_ptr error) mutable {
+ // Reattach, the callback comes from a different thread
+ android::UniqueEnv env = android::AttachEnv();
+
+ if (error) {
+ OfflineRegionInvalidateCallback::onError(*env, *callback, error);
+ } else {
+ OfflineRegionInvalidateCallback::onInvalidate(*env, *callback);
+ }
+ });
+}
+
void OfflineRegion::updateOfflineRegionMetadata(jni::JNIEnv& env_, const jni::Array<jni::jbyte>& jMetadata, const jni::Object<OfflineRegionUpdateMetadataCallback>& callback_) {
auto metadata = OfflineRegion::metadata(env_, jMetadata);
auto globalCallback = jni::NewGlobal<jni::EnvAttachingDeleter>(env_, callback_);
@@ -182,6 +200,7 @@ void OfflineRegion::registerNative(jni::JNIEnv& env) {
jni::Class<OfflineRegionStatusCallback>::Singleton(env);
jni::Class<OfflineRegionDeleteCallback>::Singleton(env);
jni::Class<OfflineRegionUpdateMetadataCallback>::Singleton(env);
+ jni::Class<OfflineRegionInvalidateCallback>::Singleton(env);
static auto& javaClass = jni::Class<OfflineRegion>::Singleton(env);
@@ -195,6 +214,7 @@ void OfflineRegion::registerNative(jni::JNIEnv& env) {
METHOD(&OfflineRegion::setOfflineRegionDownloadState, "setOfflineRegionDownloadState"),
METHOD(&OfflineRegion::getOfflineRegionStatus, "getOfflineRegionStatus"),
METHOD(&OfflineRegion::deleteOfflineRegion, "deleteOfflineRegion"),
+ METHOD(&OfflineRegion::invalidateOfflineRegion, "invalidateOfflineRegion"),
METHOD(&OfflineRegion::updateOfflineRegionMetadata, "updateOfflineRegionMetadata")
);
}
@@ -260,5 +280,21 @@ void OfflineRegion::OfflineRegionUpdateMetadataCallback::onUpdate(jni::JNIEnv& e
callback.Call(env, method, OfflineRegion::metadata(env, std::move(*metadata)));
}
+// OfflineRegionInvalidateCallback //
+
+void OfflineRegion::OfflineRegionInvalidateCallback::onError(jni::JNIEnv& env,
+ const jni::Object<OfflineRegion::OfflineRegionInvalidateCallback>& callback,
+ std::exception_ptr error) {
+ static auto& javaClass = jni::Class<OfflineRegion::OfflineRegionInvalidateCallback>::Singleton(env);
+ static auto method = javaClass.GetMethod<void (jni::String)>(env, "onError");
+ callback.Call(env, method, jni::Make<jni::String>(env, mbgl::util::toString(error)));
+}
+
+void OfflineRegion::OfflineRegionInvalidateCallback::onInvalidate(jni::JNIEnv& env, const jni::Object<OfflineRegion::OfflineRegionInvalidateCallback>& callback) {
+ static auto& javaClass = jni::Class<OfflineRegion::OfflineRegionInvalidateCallback>::Singleton(env);
+ static auto method = javaClass.GetMethod<void ()>(env, "onInvalidate");
+ callback.Call(env, method);
+}
+
} // namespace android
} // namespace mbgl
diff --git a/platform/android/src/offline/offline_region.hpp b/platform/android/src/offline/offline_region.hpp
index 4618e1abbd..dda253469e 100644
--- a/platform/android/src/offline/offline_region.hpp
+++ b/platform/android/src/offline/offline_region.hpp
@@ -37,6 +37,15 @@ public:
static void onDelete(jni::JNIEnv&, const jni::Object<OfflineRegionDeleteCallback>&);
};
+ class OfflineRegionInvalidateCallback {
+ public:
+ static constexpr auto Name() { return "com/mapbox/mapboxsdk/offline/OfflineRegion$OfflineRegionInvalidateCallback"; };
+
+ static void onError(jni::JNIEnv&, const jni::Object<OfflineRegionInvalidateCallback>&, std::exception_ptr);
+
+ static void onInvalidate(jni::JNIEnv&, const jni::Object<OfflineRegionInvalidateCallback>&);
+ };
+
class OfflineRegionUpdateMetadataCallback {
public:
static constexpr auto Name() { return "com/mapbox/mapboxsdk/offline/OfflineRegion$OfflineRegionUpdateMetadataCallback"; };
@@ -62,6 +71,8 @@ public:
void deleteOfflineRegion(jni::JNIEnv&, const jni::Object<OfflineRegionDeleteCallback>&);
+ void invalidateOfflineRegion(jni::JNIEnv&, const jni::Object<OfflineRegionInvalidateCallback>&);
+
void updateOfflineRegionMetadata(jni::JNIEnv&, const jni::Array<jni::jbyte>&, const jni::Object<OfflineRegionUpdateMetadataCallback>&);
static jni::Local<jni::Object<OfflineRegion>> New(jni::JNIEnv&, const jni::Object<FileSource>&, mbgl::OfflineRegion);
diff --git a/platform/android/src/text/format_number.cpp b/platform/android/src/text/format_number.cpp
new file mode 100644
index 0000000000..3a41175ecc
--- /dev/null
+++ b/platform/android/src/text/format_number.cpp
@@ -0,0 +1,81 @@
+#include <mbgl/style/expression/collator.hpp>
+#include <mbgl/text/language_tag.hpp>
+#include <mbgl/util/platform.hpp>
+
+#include <jni/jni.hpp>
+
+#include "../attach_env.hpp"
+#include "format_number_jni.hpp"
+
+namespace mbgl {
+namespace android {
+
+void NumberFormat::registerNative(jni::JNIEnv& env) {
+ jni::Class<NumberFormat>::Singleton(env);
+}
+
+jni::Local<jni::Object<NumberFormat>> NumberFormat::getInstance(jni::JNIEnv& env, const jni::Object<Locale>& locale) {
+ static auto& javaClass = jni::Class<NumberFormat>::Singleton(env);
+ static auto method = javaClass.GetStaticMethod<jni::Object<NumberFormat> (jni::Object<Locale>)>(env, "getInstance");
+ return javaClass.Call(env, method, locale);
+}
+
+jni::Local<jni::Object<NumberFormat>> NumberFormat::getCurrencyInstance(jni::JNIEnv& env, const jni::Object<Locale>& locale) {
+ static auto& javaClass = jni::Class<NumberFormat>::Singleton(env);
+ static auto method = javaClass.GetStaticMethod<jni::Object<NumberFormat> (jni::Object<Locale>)>(env, "getCurrencyInstance");
+ return javaClass.Call(env, method, locale);
+}
+
+jni::Local<jni::String> NumberFormat::format(jni::JNIEnv& env, const jni::Object<NumberFormat>& nf, jni::jdouble number) {
+ static auto& javaClass = jni::Class<NumberFormat>::Singleton(env);
+ static auto method = javaClass.GetMethod<jni::String (jni::jdouble)>(env, "format");
+ return nf.Call(env, method, number);
+}
+
+void NumberFormat::setMinimumFractionDigits(jni::JNIEnv& env, const jni::Object<NumberFormat>& nf, jni::jint value) {
+ static auto& javaClass = jni::Class<NumberFormat>::Singleton(env);
+ static auto method = javaClass.GetMethod<void (jni::jint)>(env, "setMinimumFractionDigits");
+ return nf.Call(env, method, value);
+}
+
+void NumberFormat::setMaximumFractionDigits(jni::JNIEnv& env, const jni::Object<NumberFormat>& nf, jni::jint value) {
+ static auto& javaClass = jni::Class<NumberFormat>::Singleton(env);
+ static auto method = javaClass.GetMethod<void (jni::jint)>(env, "setMaximumFractionDigits");
+ return nf.Call(env, method, value);
+}
+
+} // namespace android
+
+namespace platform {
+
+std::string formatNumber(double number, const std::string& localeId, const std::string& currency,
+ uint8_t minFractionDigits, uint8_t maxFractionDigits) {
+
+ auto env{ android::AttachEnv() };
+
+ jni::Global<jni::Object<android::Locale>> locale;
+ LanguageTag languageTag = !localeId.empty() ? LanguageTag::fromBCP47(localeId) : LanguageTag();
+ if (!languageTag.language) {
+ locale = jni::NewGlobal(*env, android::Locale::getDefault(*env));
+ } else if (!languageTag.region) {
+ locale = jni::NewGlobal(*env, android::Locale::New(*env, jni::Make<jni::String>(*env, *languageTag.language)));
+ } else {
+ locale = jni::NewGlobal(*env, android::Locale::New(*env, jni::Make<jni::String>(*env, *languageTag.language),
+ jni::Make<jni::String>(*env, *languageTag.region)));
+ }
+
+ jni::Global<jni::Object<android::NumberFormat>> formatter;
+ if (currency.empty()) {
+ formatter = jni::NewGlobal(*env, android::NumberFormat::getInstance(*env, locale));
+ android::NumberFormat::setMinimumFractionDigits(*env, formatter, static_cast<jni::jint>(minFractionDigits));
+ android::NumberFormat::setMaximumFractionDigits(*env, formatter, static_cast<jni::jint>(maxFractionDigits));
+ } else {
+ formatter = jni::NewGlobal(*env, android::NumberFormat::getCurrencyInstance(*env, locale));
+ }
+
+ auto result = android::NumberFormat::format(*env, formatter, static_cast<jni::jdouble>(number));
+ return jni::Make<std::string>(*env, result);
+}
+
+} // namespace platform
+} // namespace mbgl
diff --git a/platform/android/src/text/format_number_jni.hpp b/platform/android/src/text/format_number_jni.hpp
new file mode 100644
index 0000000000..1720038925
--- /dev/null
+++ b/platform/android/src/text/format_number_jni.hpp
@@ -0,0 +1,29 @@
+#pragma once
+
+#include <jni/jni.hpp>
+
+#include "collator_jni.hpp"
+
+/*
+ android::NumberFormat is the JNI wrapper
+ of java/text/NumberFormat.
+ */
+
+namespace mbgl {
+namespace android {
+
+class NumberFormat {
+public:
+ static constexpr auto Name() { return "java/text/NumberFormat"; };
+
+ static jni::Local<jni::Object<NumberFormat>> getInstance(jni::JNIEnv&, const jni::Object<Locale>&);
+ static jni::Local<jni::Object<NumberFormat>> getCurrencyInstance(jni::JNIEnv&, const jni::Object<Locale>&);
+ static jni::Local<jni::String> format(jni::JNIEnv&, const jni::Object<NumberFormat>&, jni::jdouble);
+ static void setMinimumFractionDigits(jni::JNIEnv&, const jni::Object<NumberFormat>&, jni::jint);
+ static void setMaximumFractionDigits(jni::JNIEnv&, const jni::Object<NumberFormat>&, jni::jint);
+
+ static void registerNative(jni::JNIEnv&);
+};
+
+} // namespace android
+} // namespace mbgl
diff --git a/platform/darwin/src/MGLAccountManager.h b/platform/darwin/src/MGLAccountManager.h
index b539b2ee8c..0d833a7ba7 100644
--- a/platform/darwin/src/MGLAccountManager.h
+++ b/platform/darwin/src/MGLAccountManager.h
@@ -36,8 +36,6 @@ MGL_EXPORT
*/
@property (class, nullable) NSString *accessToken;
-+ (BOOL)mapboxMetricsEnabledSettingShownInApp __attribute__((unavailable("Telemetry settings are now always shown in the ℹ️ menu.")));
-
@end
NS_ASSUME_NONNULL_END
diff --git a/platform/darwin/src/MGLOfflineStorage.h b/platform/darwin/src/MGLOfflineStorage.h
index a140201be8..b48f2ebf2c 100644
--- a/platform/darwin/src/MGLOfflineStorage.h
+++ b/platform/darwin/src/MGLOfflineStorage.h
@@ -18,8 +18,8 @@ NS_ASSUME_NONNULL_BEGIN
The `object` is the `MGLOfflinePack` object whose progress changed. The
`userInfo` dictionary contains the pack’s current state in the
- `MGLOfflinePackStateUserInfoKey` key and details about the pack’s current
- progress in the `MGLOfflinePackProgressUserInfoKey` key. You may also consult
+ `MGLOfflinePackUserInfoKeyState` key and details about the pack’s current
+ progress in the `MGLOfflinePackUserInfoKeyProgress` key. You may also consult
the `MGLOfflinePack.state` and `MGLOfflinePack.progress` properties, which
provide the same values.
@@ -43,7 +43,7 @@ FOUNDATION_EXTERN MGL_EXPORT const NSNotificationName MGLOfflinePackProgressChan
The `object` is the `MGLOfflinePack` object that encountered the error. The
`userInfo` dictionary contains the error object in the
- `MGLOfflinePackErrorUserInfoKey` key.
+ `MGLOfflinePackUserInfoKeyError` key.
*/
FOUNDATION_EXTERN MGL_EXPORT const NSNotificationName MGLOfflinePackErrorNotification;
@@ -53,7 +53,7 @@ FOUNDATION_EXTERN MGL_EXPORT const NSNotificationName MGLOfflinePackErrorNotific
The `object` is the `MGLOfflinePack` object that reached the tile limit in the
course of downloading. The `userInfo` dictionary contains the tile limit in the
- `MGLOfflinePackMaximumCountUserInfoKey` key.
+ `MGLOfflinePackUserInfoKeyMaximumCount` key.
Once this limit is reached, no instance of `MGLOfflinePack` can download
additional tiles from Mapbox APIs until already downloaded tiles are removed by
@@ -75,8 +75,6 @@ typedef NSString *MGLOfflinePackUserInfoKey NS_EXTENSIBLE_STRING_ENUM;
*/
FOUNDATION_EXTERN MGL_EXPORT const MGLOfflinePackUserInfoKey MGLOfflinePackUserInfoKeyState;
-FOUNDATION_EXTERN MGL_EXPORT NSString * const MGLOfflinePackStateUserInfoKey __attribute__((unavailable("Use MGLOfflinePackUserInfoKeyState")));
-
/**
The key for an `NSValue` object that indicates an offline pack’s current
progress. This key is used in the `userInfo` dictionary of an
@@ -86,8 +84,6 @@ FOUNDATION_EXTERN MGL_EXPORT NSString * const MGLOfflinePackStateUserInfoKey __a
*/
FOUNDATION_EXTERN MGL_EXPORT const MGLOfflinePackUserInfoKey MGLOfflinePackUserInfoKeyProgress;
-FOUNDATION_EXTERN MGL_EXPORT NSString * const MGLOfflinePackProgressUserInfoKey __attribute__((unavailable("Use MGLOfflinePackUserInfoKeyProgress")));
-
/**
The key for an `NSError` object that is encountered in the course of
downloading an offline pack. This key is used in the `userInfo` dictionary of
@@ -96,8 +92,6 @@ FOUNDATION_EXTERN MGL_EXPORT NSString * const MGLOfflinePackProgressUserInfoKey
*/
FOUNDATION_EXTERN MGL_EXPORT const MGLOfflinePackUserInfoKey MGLOfflinePackUserInfoKeyError;
-FOUNDATION_EXTERN MGL_EXPORT NSString * const MGLOfflinePackErrorUserInfoKey __attribute__((unavailable("Use MGLOfflinePackUserInfoKeyError")));
-
/**
The key for an `NSNumber` object that indicates the maximum number of
Mapbox-hosted tiles that may be downloaded and stored on the current device.
@@ -108,8 +102,6 @@ FOUNDATION_EXTERN MGL_EXPORT NSString * const MGLOfflinePackErrorUserInfoKey __a
*/
FOUNDATION_EXTERN MGL_EXPORT const MGLOfflinePackUserInfoKey MGLOfflinePackUserInfoKeyMaximumCount;
-FOUNDATION_EXTERN MGL_EXPORT NSString * const MGLOfflinePackMaximumCountUserInfoKey __attribute__((unavailable("Use MGLOfflinePackUserInfoKeyMaximumCount")));
-
FOUNDATION_EXTERN MGL_EXPORT MGLExceptionName const MGLUnsupportedRegionTypeException;
/**
diff --git a/platform/darwin/src/MGLStyle.h b/platform/darwin/src/MGLStyle.h
index 3b855ec2ab..ac5f749920 100644
--- a/platform/darwin/src/MGLStyle.h
+++ b/platform/darwin/src/MGLStyle.h
@@ -94,8 +94,6 @@ MGL_EXPORT
*/
+ (NSURL *)streetsStyleURLWithVersion:(NSInteger)version;
-+ (NSURL *)emeraldStyleURL __attribute__((unavailable("Create an NSURL object with the string “mapbox://styles/mapbox/emerald-v8”.")));
-
/**
Returns the URL to the current version of the
<a href="https://www.mapbox.com/maps/outdoors/">Mapbox Outdoors</a> style as of
@@ -201,9 +199,6 @@ MGL_EXPORT
*/
+ (NSURL *)satelliteStyleURLWithVersion:(NSInteger)version;
-
-+ (NSURL *)hybridStyleURL __attribute__((unavailable("Use -satelliteStreetsStyleURL.")));
-
/**
Returns the URL to the current version of the
<a href="https://www.mapbox.com/maps/satellite/">Mapbox Satellite Streets</a>
@@ -239,15 +234,6 @@ MGL_EXPORT
*/
+ (NSURL *)satelliteStreetsStyleURLWithVersion:(NSInteger)version;
-
-+ (NSURL *)trafficDayStyleURL __attribute__((unavailable("Create an NSURL object with the string “mapbox://styles/mapbox/traffic-day-v2”.")));
-
-+ (NSURL *)trafficDayStyleURLWithVersion:(NSInteger)version __attribute__((unavailable("Create an NSURL object with the string “mapbox://styles/mapbox/traffic-day-v2”.")));;
-
-+ (NSURL *)trafficNightStyleURL __attribute__((unavailable("Create an NSURL object with the string “mapbox://styles/mapbox/traffic-night-v2”.")));
-
-+ (NSURL *)trafficNightStyleURLWithVersion:(NSInteger)version __attribute__((unavailable("Create an NSURL object with the string “mapbox://styles/mapbox/traffic-night-v2”.")));
-
#pragma mark Accessing Metadata About the Style
/**
@@ -473,17 +459,6 @@ MGL_EXPORT
*/
- (void)removeLayer:(MGLStyleLayer *)layer;
-#pragma mark Managing Style Classes
-
-
-@property (nonatomic) NSArray<NSString *> *styleClasses __attribute__((unavailable("Support for style classes has been removed.")));
-
-- (BOOL)hasStyleClass:(NSString *)styleClass __attribute__((unavailable("Support for style classes has been removed.")));
-
-- (void)addStyleClass:(NSString *)styleClass __attribute__((unavailable("Support for style classes has been removed.")));
-
-- (void)removeStyleClass:(NSString *)styleClass __attribute__((unavailable("Support for style classes has been removed.")));
-
#pragma mark Managing a Style’s Images
/**
@@ -562,8 +537,6 @@ MGL_EXPORT
*/
- (void)localizeLabelsIntoLocale:(nullable NSLocale *)locale;
-@property (nonatomic) BOOL localizesLabels __attribute__((unavailable("Use -localizeLabelsIntoLocale: instead.")));
-
@end
NS_ASSUME_NONNULL_END
diff --git a/platform/darwin/src/run_loop.cpp b/platform/darwin/src/run_loop.cpp
index 0778b004e5..0e22dc9e33 100644
--- a/platform/darwin/src/run_loop.cpp
+++ b/platform/darwin/src/run_loop.cpp
@@ -25,7 +25,6 @@ RunLoop::RunLoop(Type)
}
RunLoop::~RunLoop() {
- assert(Scheduler::GetCurrent());
Scheduler::SetCurrent(nullptr);
}
@@ -38,6 +37,7 @@ void RunLoop::run() {
}
void RunLoop::runOnce() {
+ wake();
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, false);
}
diff --git a/platform/darwin/src/string_nsstring.mm b/platform/darwin/src/string_nsstring.mm
index 08f9aeccef..096ed2b212 100644
--- a/platform/darwin/src/string_nsstring.mm
+++ b/platform/darwin/src/string_nsstring.mm
@@ -27,5 +27,28 @@ std::string lowercase(const std::string &string) {
return result;
}
+std::string formatNumber(double number, const std::string& localeId, const std::string& currency,
+ uint8_t minFractionDigits, uint8_t maxFractionDigits) {
+
+ static NSNumberFormatter *numberFormatter;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ numberFormatter = [[NSNumberFormatter alloc] init];
+ });
+
+ numberFormatter.locale = !localeId.empty() ? [NSLocale localeWithLocaleIdentifier:@(localeId.c_str())] : nil;
+ numberFormatter.currencyCode = !currency.empty() ? @(currency.c_str()) : nil;
+ if (currency.empty()) {
+ numberFormatter.minimumFractionDigits = minFractionDigits;
+ numberFormatter.maximumFractionDigits = maxFractionDigits;
+ numberFormatter.numberStyle = NSNumberFormatterDecimalStyle;
+ } else {
+ numberFormatter.numberStyle = NSNumberFormatterCurrencyStyle;
+ }
+ NSString *formatted = [numberFormatter stringFromNumber:@(number)];
+ std::string result = std::string([formatted UTF8String]);
+ return result;
+}
+
}
}
diff --git a/platform/darwin/test/MGLClockDirectionFormatterTests.m b/platform/darwin/test/MGLClockDirectionFormatterTests.m
index 13e12ae2f2..1b2f058d18 100644
--- a/platform/darwin/test/MGLClockDirectionFormatterTests.m
+++ b/platform/darwin/test/MGLClockDirectionFormatterTests.m
@@ -1,14 +1,17 @@
#import <Mapbox/Mapbox.h>
#import <XCTest/XCTest.h>
-static NSString * const MGLTestLocaleIdentifier = @"en-US";
-
@interface MGLClockDirectionFormatterTests : XCTestCase
@end
@implementation MGLClockDirectionFormatterTests
+- (void)setUp {
+ // FIXME: https://github.com/mapbox/mapbox-gl-native/issues/14908
+ XCTAssertEqualObjects(NSLocale.currentLocale.localeIdentifier, @"en_US", @"Device locale must be en_US for these tests to pass.");
+}
+
- (void)testClockDirections {
MGLClockDirectionFormatter *shortFormatter = [[MGLClockDirectionFormatter alloc] init];
shortFormatter.unitStyle = NSFormattingUnitStyleShort;
diff --git a/platform/darwin/test/MGLCompassDirectionFormatterTests.m b/platform/darwin/test/MGLCompassDirectionFormatterTests.m
index c4ccc6ac4f..cd3aaebab7 100644
--- a/platform/darwin/test/MGLCompassDirectionFormatterTests.m
+++ b/platform/darwin/test/MGLCompassDirectionFormatterTests.m
@@ -7,6 +7,11 @@
@implementation MGLCompassDirectionFormatterTests
+- (void)setUp {
+ // FIXME: https://github.com/mapbox/mapbox-gl-native/issues/14908
+ XCTAssertEqualObjects(NSLocale.currentLocale.localeIdentifier, @"en_US", @"Device locale must be en_US for these tests to pass.");
+}
+
- (void)testCompassDirections {
MGLCompassDirectionFormatter *shortFormatter = [[MGLCompassDirectionFormatter alloc] init];
shortFormatter.unitStyle = NSFormattingUnitStyleShort;
diff --git a/platform/darwin/test/MGLCoordinateFormatterTests.m b/platform/darwin/test/MGLCoordinateFormatterTests.m
index ac083fa103..4d4d8cf971 100644
--- a/platform/darwin/test/MGLCoordinateFormatterTests.m
+++ b/platform/darwin/test/MGLCoordinateFormatterTests.m
@@ -7,6 +7,11 @@
@implementation MGLCoordinateFormatterTests
+- (void)setUp {
+ // FIXME: https://github.com/mapbox/mapbox-gl-native/issues/14908
+ XCTAssertEqualObjects(NSLocale.currentLocale.localeIdentifier, @"en_US", @"Device locale must be en_US for these tests to pass.");
+}
+
- (void)testStrings {
MGLCoordinateFormatter *shortFormatter = [[MGLCoordinateFormatter alloc] init];
shortFormatter.unitStyle = NSFormattingUnitStyleShort;
diff --git a/platform/darwin/test/MGLMapViewTests.m b/platform/darwin/test/MGLMapViewTests.m
index 9a8e7bdb64..9ad7016a61 100644
--- a/platform/darwin/test/MGLMapViewTests.m
+++ b/platform/darwin/test/MGLMapViewTests.m
@@ -1,5 +1,12 @@
#import <Mapbox/Mapbox.h>
#import <XCTest/XCTest.h>
+#import <TargetConditionals.h>
+
+#if TARGET_OS_IPHONE
+ #define MGLEdgeInsetsZero UIEdgeInsetsZero
+#else
+ #define MGLEdgeInsetsZero NSEdgeInsetsZero
+#endif
static MGLMapView *mapView;
@@ -27,13 +34,13 @@ static MGLMapView *mapView;
MGLCoordinateBounds leftAntimeridianBounds = MGLCoordinateBoundsMake(CLLocationCoordinate2DMake(-75, 175), CLLocationCoordinate2DMake(75, 180));
CGRect leftAntimeridianBoundsRect = [mapView convertCoordinateBounds:leftAntimeridianBounds toRectToView:mapView];
-
+
MGLCoordinateBounds rightAntimeridianBounds = MGLCoordinateBoundsMake(CLLocationCoordinate2DMake(-75, -180), CLLocationCoordinate2DMake(75, -175));
CGRect rightAntimeridianBoundsRect = [mapView convertCoordinateBounds:rightAntimeridianBounds toRectToView:mapView];
-
+
MGLCoordinateBounds spanningBounds = MGLCoordinateBoundsMake(CLLocationCoordinate2DMake(24, 140), CLLocationCoordinate2DMake(44, 240));
CGRect spanningBoundsRect = [mapView convertCoordinateBounds:spanningBounds toRectToView:mapView];
-
+
// If the resulting CGRect from -convertCoordinateBounds:toRectToView:
// intersects the set of bounds to the left and right of the
// antimeridian, then we know that the CGRect spans across the antimeridian
@@ -41,4 +48,108 @@ static MGLMapView *mapView;
XCTAssertTrue(CGRectIntersectsRect(spanningBoundsRect, rightAntimeridianBoundsRect), @"Something");
}
+#if TARGET_OS_IPHONE
+- (void)testUserTrackingModeCompletion {
+ __block BOOL completed = NO;
+ [mapView setUserTrackingMode:MGLUserTrackingModeNone animated:NO completionHandler:^{
+ completed = YES;
+ }];
+ XCTAssertTrue(completed, @"Completion block should get called synchronously when the mode is unchanged.");
+
+ completed = NO;
+ [mapView setUserTrackingMode:MGLUserTrackingModeNone animated:YES completionHandler:^{
+ completed = YES;
+ }];
+ XCTAssertTrue(completed, @"Completion block should get called synchronously when the mode is unchanged.");
+
+ completed = NO;
+ [mapView setUserTrackingMode:MGLUserTrackingModeFollow animated:NO completionHandler:^{
+ completed = YES;
+ }];
+ XCTAssertTrue(completed, @"Completion block should get called synchronously when there’s no location.");
+
+ completed = NO;
+ [mapView setUserTrackingMode:MGLUserTrackingModeFollowWithHeading animated:YES completionHandler:^{
+ completed = YES;
+ }];
+ XCTAssertTrue(completed, @"Completion block should get called synchronously when there’s no location.");
+}
+
+- (void)testTargetCoordinateCompletion {
+ __block BOOL completed = NO;
+ [mapView setTargetCoordinate:kCLLocationCoordinate2DInvalid animated:NO completionHandler:^{
+ completed = YES;
+ }];
+ XCTAssertTrue(completed, @"Completion block should get called synchronously when the target coordinate is unchanged.");
+
+ completed = NO;
+ [mapView setTargetCoordinate:kCLLocationCoordinate2DInvalid animated:YES completionHandler:^{
+ completed = YES;
+ }];
+ XCTAssertTrue(completed, @"Completion block should get called synchronously when the target coordinate is unchanged.");
+
+ completed = NO;
+ [mapView setUserTrackingMode:MGLUserTrackingModeFollow animated:NO completionHandler:nil];
+ [mapView setTargetCoordinate:CLLocationCoordinate2DMake(39.128106, -84.516293) animated:YES completionHandler:^{
+ completed = YES;
+ }];
+ XCTAssertTrue(completed, @"Completion block should get called synchronously when not tracking user course.");
+
+ completed = NO;
+ [mapView setUserTrackingMode:MGLUserTrackingModeFollowWithCourse animated:NO completionHandler:nil];
+ [mapView setTargetCoordinate:CLLocationCoordinate2DMake(39.224407, -84.394957) animated:YES completionHandler:^{
+ completed = YES;
+ }];
+ XCTAssertTrue(completed, @"Completion block should get called synchronously when there’s no location.");
+}
+#endif
+
+- (void)testVisibleCoordinatesCompletion {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Completion block should get called when not animated"];
+ MGLCoordinateBounds unitBounds = MGLCoordinateBoundsMake(CLLocationCoordinate2DMake(0, 0), CLLocationCoordinate2DMake(1, 1));
+ [mapView setVisibleCoordinateBounds:unitBounds edgePadding:MGLEdgeInsetsZero animated:NO completionHandler:^{
+ [expectation fulfill];
+ }];
+ [self waitForExpectations:@[expectation] timeout:1];
+
+#if TARGET_OS_IPHONE
+ expectation = [self expectationWithDescription:@"Completion block should get called when animated"];
+ CLLocationCoordinate2D antiunitCoordinates[] = {
+ CLLocationCoordinate2DMake(0, 0),
+ CLLocationCoordinate2DMake(-1, -1),
+ };
+ [mapView setVisibleCoordinates:antiunitCoordinates
+ count:sizeof(antiunitCoordinates) / sizeof(antiunitCoordinates[0])
+ edgePadding:UIEdgeInsetsZero
+ direction:0
+ duration:0
+ animationTimingFunction:nil
+ completionHandler:^{
+ [expectation fulfill];
+ }];
+ [self waitForExpectations:@[expectation] timeout:1];
+#endif
+}
+
+- (void)testShowAnnotationsCompletion {
+ __block BOOL completed = NO;
+ [mapView showAnnotations:@[] edgePadding:MGLEdgeInsetsZero animated:NO completionHandler:^{
+ completed = YES;
+ }];
+ XCTAssertTrue(completed, @"Completion block should get called synchronously when there are no annotations to show.");
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Completion block should get called when not animated"];
+ MGLPointAnnotation *annotation = [[MGLPointAnnotation alloc] init];
+ [mapView showAnnotations:@[annotation] edgePadding:MGLEdgeInsetsZero animated:NO completionHandler:^{
+ [expectation fulfill];
+ }];
+ [self waitForExpectations:@[expectation] timeout:1];
+
+ expectation = [self expectationWithDescription:@"Completion block should get called when animated."];
+ [mapView showAnnotations:@[annotation] edgePadding:MGLEdgeInsetsZero animated:YES completionHandler:^{
+ [expectation fulfill];
+ }];
+ [self waitForExpectations:@[expectation] timeout:1];
+}
+
@end
diff --git a/platform/darwin/test/MGLNSStringAdditionsTests.m b/platform/darwin/test/MGLNSStringAdditionsTests.m
index 571cf49d7f..872a8ce664 100644
--- a/platform/darwin/test/MGLNSStringAdditionsTests.m
+++ b/platform/darwin/test/MGLNSStringAdditionsTests.m
@@ -9,7 +9,7 @@
@implementation MGLNSStringAdditionsTests
- (void)testTitleCasedString {
- NSLocale *locale = [NSLocale currentLocale];
+ NSLocale *locale = [NSLocale localeWithLocaleIdentifier:@"en_US"];
XCTAssertEqualObjects([@"© OpenStreetMap" mgl_titleCasedStringWithLocale:locale], @"© OpenStreetMap");
XCTAssertEqualObjects([@"© OSM" mgl_titleCasedStringWithLocale:locale], @"© OSM");
diff --git a/platform/darwin/test/MGLOfflineStorageTests.mm b/platform/darwin/test/MGLOfflineStorageTests.mm
index 86dc28eb04..5551d8889b 100644
--- a/platform/darwin/test/MGLOfflineStorageTests.mm
+++ b/platform/darwin/test/MGLOfflineStorageTests.mm
@@ -285,35 +285,16 @@
}
- (void)testAddFileContent {
-
- NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
- NSString *documentDir = [documentPaths objectAtIndex:0];
NSFileManager *fileManager = [NSFileManager defaultManager];
-
- BOOL directoryExists = [fileManager fileExistsAtPath:documentDir];
- if (!directoryExists) {
- [fileManager createDirectoryAtPath:documentDir withIntermediateDirectories:YES attributes:nil error:nil];
- }
-
+
// Valid database
- {
+ [XCTContext runActivityNamed:@"Valid database" block:^(id<XCTActivity> activity) {
NSURL *resourceURL = [NSURL fileURLWithPath:[[NSBundle bundleForClass:[self class]] pathForResource:@"sideload_sat" ofType:@"db"]];
- NSString *filePath = [documentDir stringByAppendingPathComponent:@"sideload_sat.db"];
-
- BOOL databaseExists = [fileManager fileExistsAtPath:filePath];
- if (databaseExists) {
- [fileManager removeItemAtPath:filePath error:nil];
- }
-
+
NSError *error;
- [fileManager moveItemAtURL:resourceURL toURL:[NSURL fileURLWithPath:filePath] error:&error];
- NSDictionary *atributes = @{ NSFilePosixPermissions: @0777 };
- error = nil;
- [fileManager setAttributes:atributes ofItemAtPath:filePath error:&error];
- XCTAssertNil(error, @"Changing the file's permissions:%@ should not return an error.", filePath);
- error = nil;
- NSDictionary *fileAttributes = [fileManager attributesOfItemAtPath:filePath error:&error];
-
+ NSDictionary *fileAttributes = [fileManager attributesOfItemAtPath:resourceURL.path error:&error];
+ XCTAssertNil(error, @"Getting the file's attributes should not return an error. (%@)", resourceURL.path);
+
NSNumber *fileSizeNumber = [fileAttributes objectForKey:NSFileSize];
long long fileSize = [fileSizeNumber longLongValue];
long long databaseFileSize = 73728;
@@ -330,7 +311,7 @@
XCTestExpectation *fileAdditionCompletionHandlerExpectation = [self expectationWithDescription:@"add database content completion handler"];
MGLOfflineStorage *os = [MGLOfflineStorage sharedOfflineStorage];
- [os addContentsOfFile:filePath withCompletionHandler:^(NSURL *fileURL, NSArray<MGLOfflinePack *> * _Nullable packs, NSError * _Nullable error) {
+ [os addContentsOfURL:resourceURL withCompletionHandler:^(NSURL *fileURL, NSArray<MGLOfflinePack *> * _Nullable packs, NSError * _Nullable error) {
XCTAssertNotNil(fileURL, @"The fileURL should not be nil.");
XCTAssertNotNil(packs, @"Adding the contents of the sideload_sat.db should update one pack.");
XCTAssertNil(error, @"Adding contents to a file should not return an error.");
@@ -342,58 +323,37 @@
[self waitForExpectationsWithTimeout:10 handler:nil];
// Depending on the database it may update or add a pack. For this case specifically the offline database adds one pack.
XCTAssertEqual([MGLOfflineStorage sharedOfflineStorage].packs.count, countOfPacks + 1, @"Adding contents of sideload_sat.db should add one pack.");
+ }];
- }
// Invalid database type
- {
+ [XCTContext runActivityNamed:@"Invalid database type" block:^(id<XCTActivity> activity) {
NSURL *resourceURL = [NSURL fileURLWithPath:[[NSBundle bundleForClass:[self class]] pathForResource:@"one-liner" ofType:@"json"]];
- NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
- NSString *documentDir = [documentPaths objectAtIndex:0];
- NSString *filePath = [documentDir stringByAppendingPathComponent:@"on-liner-copy.json"];
- NSFileManager *fileManager = [NSFileManager defaultManager];
-
- BOOL exists = [fileManager fileExistsAtPath:filePath];
- if (exists) {
- [fileManager removeItemAtPath:filePath error:nil];
- }
- NSError *error;
- [fileManager copyItemAtURL:resourceURL toURL:[NSURL fileURLWithPath:filePath] error:&error];
-
XCTestExpectation *invalidFileCompletionHandlerExpectation = [self expectationWithDescription:@"invalid content database completion handler"];
MGLOfflineStorage *os = [MGLOfflineStorage sharedOfflineStorage];
- [os addContentsOfFile:filePath withCompletionHandler:^(NSURL *fileURL, NSArray<MGLOfflinePack *> * _Nullable packs, NSError * _Nullable error) {
+ [os addContentsOfFile:resourceURL.path withCompletionHandler:^(NSURL *fileURL, NSArray<MGLOfflinePack *> * _Nullable packs, NSError * _Nullable error) {
XCTAssertNotNil(error, @"Passing an invalid offline database file should return an error.");
XCTAssertNil(packs, @"Passing an invalid offline database file should not add packs to the offline database.");
[invalidFileCompletionHandlerExpectation fulfill];
}];
[self waitForExpectationsWithTimeout:10 handler:nil];
- }
- // File non existent
- {
- NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
- NSString *documentDir = [documentPaths objectAtIndex:0];
- NSString *filePath = [documentDir stringByAppendingPathComponent:@"nonexistent.db"];
- NSFileManager *fileManager = [NSFileManager defaultManager];
-
- BOOL exists = [fileManager fileExistsAtPath:filePath];
- if (exists) {
- [fileManager removeItemAtPath:filePath error:nil];
- }
-
+ }];
+
+ // File does not exist
+ [XCTContext runActivityNamed:@"File does not exist" block:^(id<XCTActivity> activity) {
+ NSURL *resourceURL = [NSURL URLWithString:@"nonexistent.db"];
+
MGLOfflineStorage *os = [MGLOfflineStorage sharedOfflineStorage];
- XCTAssertThrowsSpecificNamed([os addContentsOfFile:filePath withCompletionHandler:nil], NSException, NSInvalidArgumentException, "MGLOfflineStorage should rise an exception if an invalid database file is passed.");
+ XCTAssertThrowsSpecificNamed([os addContentsOfURL:resourceURL withCompletionHandler:nil], NSException, NSInvalidArgumentException, "MGLOfflineStorage should rise an exception if an invalid database file is passed.");
+ }];
- }
// URL to a non-file
- {
+ [XCTContext runActivityNamed:@"URL to a non-file" block:^(id<XCTActivity> activity) {
NSURL *resourceURL = [NSURL URLWithString:@"https://www.mapbox.com"];
MGLOfflineStorage *os = [MGLOfflineStorage sharedOfflineStorage];
XCTAssertThrowsSpecificNamed([os addContentsOfURL:resourceURL withCompletionHandler:nil], NSException, NSInvalidArgumentException, "MGLOfflineStorage should rise an exception if an invalid URL file is passed.");
-
- }
-
+ }];
}
- (void)testPutResourceForURL {
diff --git a/platform/default/include/mbgl/storage/offline_database.hpp b/platform/default/include/mbgl/storage/offline_database.hpp
index 4e9ffcf028..afce87b542 100644
--- a/platform/default/include/mbgl/storage/offline_database.hpp
+++ b/platform/default/include/mbgl/storage/offline_database.hpp
@@ -40,11 +40,11 @@ class OfflineDatabase : private util::noncopyable {
public:
// Limits affect ambient caching (put) only; resources required by offline
// regions are exempt.
- OfflineDatabase(std::string path, uint64_t maximumCacheSize = util::DEFAULT_MAX_CACHE_SIZE);
+ OfflineDatabase(std::string path);
~OfflineDatabase();
void changePath(const std::string&);
- std::exception_ptr resetCache();
+ std::exception_ptr resetDatabase();
optional<Response> get(const Resource&);
@@ -56,12 +56,12 @@ public:
// are the latest version. This is more efficient than cleaning the
// cache because if the tile is considered valid after the server
// lookup, it will not get downloaded again.
- std::exception_ptr invalidateTileCache();
+ std::exception_ptr invalidateAmbientCache();
// Clear the tile cache, freeing resources. This operation can be
// potentially slow because it will trigger a VACUUM on SQLite,
// forcing the database to move pages on the filesystem.
- std::exception_ptr clearTileCache();
+ std::exception_ptr clearAmbientCache();
expected<OfflineRegions, std::exception_ptr> listRegions();
@@ -86,6 +86,7 @@ public:
expected<OfflineRegionDefinition, std::exception_ptr> getRegionDefinition(int64_t regionID);
expected<OfflineRegionStatus, std::exception_ptr> getRegionCompletedStatus(int64_t regionID);
+ std::exception_ptr setMaximumAmbientCacheSize(uint64_t);
void setOfflineMapboxTileCountLimit(uint64_t);
uint64_t getOfflineMapboxTileCountLimit();
bool offlineMapboxTileCountLimitExceeded();
@@ -104,6 +105,7 @@ private:
void migrateToVersion3();
void migrateToVersion6();
void cleanup();
+ bool disabled();
mapbox::sqlite::Statement& getStatement(const char *);
@@ -136,9 +138,9 @@ private:
template <class T>
T getPragma(const char *);
- uint64_t maximumCacheSize;
-
+ uint64_t maximumAmbientCacheSize = util::DEFAULT_MAX_CACHE_SIZE;
uint64_t offlineMapboxTileCountLimit = util::mapbox::DEFAULT_OFFLINE_TILE_COUNT_LIMIT;
+
optional<uint64_t> offlineMapboxTileCount;
bool evict(uint64_t neededFreeSize);
diff --git a/platform/default/src/mbgl/gfx/headless_frontend.cpp b/platform/default/src/mbgl/gfx/headless_frontend.cpp
index 0e97c40e49..f5e6b7f482 100644
--- a/platform/default/src/mbgl/gfx/headless_frontend.cpp
+++ b/platform/default/src/mbgl/gfx/headless_frontend.cpp
@@ -132,19 +132,24 @@ PremultipliedImage HeadlessFrontend::readStillImage() {
PremultipliedImage HeadlessFrontend::render(Map& map) {
PremultipliedImage result;
+ std::exception_ptr error;
- map.renderStill([&](std::exception_ptr error) {
- if (error) {
- std::rethrow_exception(error);
+ map.renderStill([&](std::exception_ptr e) {
+ if (e) {
+ error = e;
} else {
result = backend->readStillImage();
}
});
- while (!result.valid()) {
+ while (!result.valid() && !error) {
util::RunLoop::Get()->runOnce();
}
+ if (error) {
+ std::rethrow_exception(error);
+ }
+
return result;
}
diff --git a/platform/default/src/mbgl/storage/default_file_source.cpp b/platform/default/src/mbgl/storage/default_file_source.cpp
index 92e86b25d0..dc4b2908a8 100644
--- a/platform/default/src/mbgl/storage/default_file_source.cpp
+++ b/platform/default/src/mbgl/storage/default_file_source.cpp
@@ -20,10 +20,10 @@ namespace mbgl {
class DefaultFileSource::Impl {
public:
- Impl(std::shared_ptr<FileSource> assetFileSource_, std::string cachePath, uint64_t maximumCacheSize)
+ Impl(std::shared_ptr<FileSource> assetFileSource_, std::string cachePath)
: assetFileSource(std::move(assetFileSource_))
, localFileSource(std::make_unique<LocalFileSource>())
- , offlineDatabase(std::make_unique<OfflineDatabase>(cachePath, maximumCacheSize)) {
+ , offlineDatabase(std::make_unique<OfflineDatabase>(std::move(cachePath))) {
}
void setAPIBaseURL(const std::string& url) {
@@ -184,16 +184,20 @@ public:
offlineDatabase->put(resource, response);
}
- void resetCache(std::function<void (std::exception_ptr)> callback) {
- callback(offlineDatabase->resetCache());
+ void resetDatabase(std::function<void (std::exception_ptr)> callback) {
+ callback(offlineDatabase->resetDatabase());
}
- void invalidateTileCache(std::function<void (std::exception_ptr)> callback) {
- callback(offlineDatabase->invalidateTileCache());
+ void invalidateAmbientCache(std::function<void (std::exception_ptr)> callback) {
+ callback(offlineDatabase->invalidateAmbientCache());
}
- void clearTileCache(std::function<void (std::exception_ptr)> callback) {
- callback(offlineDatabase->clearTileCache());
+ void clearAmbientCache(std::function<void (std::exception_ptr)> callback) {
+ callback(offlineDatabase->clearAmbientCache());
+ }
+
+ void setMaximumAmbientCacheSize(uint64_t size, std::function<void (std::exception_ptr)> callback) {
+ callback(offlineDatabase->setMaximumAmbientCacheSize(size));
}
private:
@@ -220,21 +224,22 @@ private:
std::unordered_map<int64_t, std::unique_ptr<OfflineDownload>> downloads;
};
-DefaultFileSource::DefaultFileSource(const std::string& cachePath,
- const std::string& assetPath,
- uint64_t maximumCacheSize)
- : DefaultFileSource(cachePath, std::make_unique<AssetFileSource>(assetPath), maximumCacheSize) {
+DefaultFileSource::DefaultFileSource(const std::string& cachePath, const std::string& assetPath, bool supportCacheOnlyRequests_)
+ : DefaultFileSource(cachePath, std::make_unique<AssetFileSource>(assetPath), supportCacheOnlyRequests_) {
}
-DefaultFileSource::DefaultFileSource(const std::string& cachePath,
- std::unique_ptr<FileSource>&& assetFileSource_,
- uint64_t maximumCacheSize)
+DefaultFileSource::DefaultFileSource(const std::string& cachePath, std::unique_ptr<FileSource>&& assetFileSource_, bool supportCacheOnlyRequests_)
: assetFileSource(std::move(assetFileSource_))
- , impl(std::make_unique<util::Thread<Impl>>("DefaultFileSource", assetFileSource, cachePath, maximumCacheSize)) {
+ , impl(std::make_unique<util::Thread<Impl>>("DefaultFileSource", assetFileSource, cachePath))
+ , supportCacheOnlyRequests(supportCacheOnlyRequests_) {
}
DefaultFileSource::~DefaultFileSource() = default;
+bool DefaultFileSource::supportsCacheOnlyRequests() const {
+ return supportCacheOnlyRequests;
+}
+
void DefaultFileSource::setAPIBaseURL(const std::string& baseURL) {
impl->actor().invoke(&Impl::setAPIBaseURL, baseURL);
@@ -339,16 +344,20 @@ void DefaultFileSource::put(const Resource& resource, const Response& response)
impl->actor().invoke(&Impl::put, resource, response);
}
-void DefaultFileSource::resetCache(std::function<void (std::exception_ptr)> callback) {
- impl->actor().invoke(&Impl::resetCache, callback);
+void DefaultFileSource::resetDatabase(std::function<void (std::exception_ptr)> callback) {
+ impl->actor().invoke(&Impl::resetDatabase, std::move(callback));
+}
+
+void DefaultFileSource::invalidateAmbientCache(std::function<void (std::exception_ptr)> callback) {
+ impl->actor().invoke(&Impl::invalidateAmbientCache, std::move(callback));
}
-void DefaultFileSource::invalidateTileCache(std::function<void (std::exception_ptr)> callback) {
- impl->actor().invoke(&Impl::invalidateTileCache, callback);
+void DefaultFileSource::clearAmbientCache(std::function<void (std::exception_ptr)> callback) {
+ impl->actor().invoke(&Impl::clearAmbientCache, std::move(callback));
}
-void DefaultFileSource::clearTileCache(std::function<void (std::exception_ptr)> callback) {
- impl->actor().invoke(&Impl::clearTileCache, callback);
+void DefaultFileSource::setMaximumAmbientCacheSize(uint64_t size, std::function<void (std::exception_ptr)> callback) {
+ impl->actor().invoke(&Impl::setMaximumAmbientCacheSize, size, std::move(callback));
}
// For testing only:
diff --git a/platform/default/src/mbgl/storage/file_source.cpp b/platform/default/src/mbgl/storage/file_source.cpp
index a7bbe82f5a..4e800cc8f4 100644
--- a/platform/default/src/mbgl/storage/file_source.cpp
+++ b/platform/default/src/mbgl/storage/file_source.cpp
@@ -6,7 +6,7 @@
namespace mbgl {
std::shared_ptr<FileSource> FileSource::createPlatformFileSource(const ResourceOptions& options) {
- auto fileSource = std::make_shared<DefaultFileSource>(options.cachePath(), options.assetPath());
+ auto fileSource = std::make_shared<DefaultFileSource>(options.cachePath(), options.assetPath(), options.supportsCacheOnlyRequests());
fileSource->setAccessToken(options.accessToken());
fileSource->setAPIBaseURL(options.baseURL());
return fileSource;
diff --git a/platform/default/src/mbgl/storage/offline_database.cpp b/platform/default/src/mbgl/storage/offline_database.cpp
index 8c16ebf078..d85b560d5a 100644
--- a/platform/default/src/mbgl/storage/offline_database.cpp
+++ b/platform/default/src/mbgl/storage/offline_database.cpp
@@ -13,9 +13,8 @@
namespace mbgl {
-OfflineDatabase::OfflineDatabase(std::string path_, uint64_t maximumCacheSize_)
- : path(std::move(path_)),
- maximumCacheSize(maximumCacheSize_) {
+OfflineDatabase::OfflineDatabase(std::string path_)
+ : path(std::move(path_)) {
try {
initialize();
} catch (const util::IOException& ex) {
@@ -88,6 +87,19 @@ void OfflineDatabase::cleanup() {
}
}
+bool OfflineDatabase::disabled() {
+ if (maximumAmbientCacheSize) {
+ return false;
+ }
+
+ auto regions = listRegions();
+ if (regions && !regions.value().empty()) {
+ return false;
+ }
+
+ return true;
+}
+
void OfflineDatabase::handleError(const mapbox::sqlite::Exception& ex, const char* action) {
if (ex.code == mapbox::sqlite::ResultCode::NotADB ||
ex.code == mapbox::sqlite::ResultCode::Corrupt ||
@@ -177,6 +189,10 @@ mapbox::sqlite::Statement& OfflineDatabase::getStatement(const char* sql) {
}
optional<Response> OfflineDatabase::get(const Resource& resource) try {
+ if (disabled()) {
+ return nullopt;
+ }
+
auto result = getInternal(resource);
return result ? optional<Response>{ result->first } : nullopt;
} catch (const util::IOException& ex) {
@@ -209,6 +225,11 @@ std::pair<bool, uint64_t> OfflineDatabase::put(const Resource& resource, const R
if (!db) {
initialize();
}
+
+ if (disabled()) {
+ return { false, 0 };
+ }
+
mapbox::sqlite::Transaction transaction(*db, mapbox::sqlite::Transaction::Immediate);
auto result = putInternal(resource, response, true);
transaction.commit();
@@ -604,9 +625,9 @@ bool OfflineDatabase::putTile(const Resource::TileData& tile,
return true;
}
-std::exception_ptr OfflineDatabase::invalidateTileCache() try {
+std::exception_ptr OfflineDatabase::invalidateAmbientCache() try {
// clang-format off
- mapbox::sqlite::Query query{ getStatement(
+ mapbox::sqlite::Query tileQuery{ getStatement(
"UPDATE tiles "
"SET expires = 0, must_revalidate = 1 "
"WHERE id NOT IN ("
@@ -615,16 +636,29 @@ std::exception_ptr OfflineDatabase::invalidateTileCache() try {
) };
// clang-format on
- query.run();
+ tileQuery.run();
+
+ // clang-format off
+ mapbox::sqlite::Query resourceQuery{ getStatement(
+ "UPDATE resources "
+ "SET expires = 0, must_revalidate = 1 "
+ "WHERE id NOT IN ("
+ " SELECT resource_id FROM region_resources"
+ ")"
+ ) };
+ // clang-format on
+
+ resourceQuery.run();
+
return nullptr;
} catch (const mapbox::sqlite::Exception& ex) {
- handleError(ex, "invalidate tile cache");
+ handleError(ex, "invalidate ambient cache");
return std::current_exception();
}
-std::exception_ptr OfflineDatabase::clearTileCache() try {
+std::exception_ptr OfflineDatabase::clearAmbientCache() try {
// clang-format off
- mapbox::sqlite::Query query{ getStatement(
+ mapbox::sqlite::Query tileQuery{ getStatement(
"DELETE FROM tiles "
"WHERE id NOT IN ("
" SELECT tile_id FROM region_tiles"
@@ -632,20 +666,31 @@ std::exception_ptr OfflineDatabase::clearTileCache() try {
) };
// clang-format on
- query.run();
+ tileQuery.run();
+
+ // clang-format off
+ mapbox::sqlite::Query resourceQuery{ getStatement(
+ "DELETE FROM resources "
+ "WHERE id NOT IN ("
+ " SELECT resource_id FROM region_resources"
+ ")"
+ ) };
+ // clang-format on
+
+ resourceQuery.run();
db->exec("VACUUM");
return nullptr;
} catch (const mapbox::sqlite::Exception& ex) {
- handleError(ex, "clear tile cache");
+ handleError(ex, "clear ambient cache");
return std::current_exception();
}
std::exception_ptr OfflineDatabase::invalidateRegion(int64_t regionID) try {
{
// clang-format off
- mapbox::sqlite::Query query{ getStatement(
+ mapbox::sqlite::Query tileQuery{ getStatement(
"UPDATE tiles "
"SET expires = 0, must_revalidate = 1 "
"WHERE id IN ("
@@ -654,8 +699,21 @@ std::exception_ptr OfflineDatabase::invalidateRegion(int64_t regionID) try {
) };
// clang-format on
- query.bind(1, regionID);
- query.run();
+ tileQuery.bind(1, regionID);
+ tileQuery.run();
+
+ // clang-format off
+ mapbox::sqlite::Query resourceQuery{ getStatement(
+ "UPDATE resources "
+ "SET expires = 0, must_revalidate = 1 "
+ "WHERE id IN ("
+ " SELECT resource_id FROM region_resources WHERE region_id = ?"
+ ")"
+ ) };
+ // clang-format on
+
+ resourceQuery.bind(1, regionID);
+ resourceQuery.run();
}
assert(db);
@@ -906,13 +964,13 @@ void OfflineDatabase::putRegionResources(int64_t regionID,
}
uint64_t OfflineDatabase::putRegionResourceInternal(int64_t regionID, const Resource& resource, const Response& response) {
- if (exceedsOfflineMapboxTileCountLimit(resource)) {
- throw MapboxTileLimitExceededException();
- }
-
uint64_t size = putInternal(resource, response, false).second;
bool previouslyUnused = markUsed(regionID, resource);
+ if (previouslyUnused && exceedsOfflineMapboxTileCountLimit(resource)) {
+ throw MapboxTileLimitExceededException();
+ }
+
if (offlineMapboxTileCount
&& resource.kind == Resource::Kind::Tile
&& util::mapbox::isMapboxURL(resource.url)
@@ -946,15 +1004,14 @@ bool OfflineDatabase::markUsed(int64_t regionID, const Resource& resource) {
insertQuery.bind(6, tile.z);
insertQuery.run();
- if (insertQuery.changes() == 0) {
- return false;
- }
+ bool notOnThisRegion = insertQuery.changes() != 0;
// clang-format off
mapbox::sqlite::Query selectQuery{ getStatement(
"SELECT region_id "
"FROM region_tiles, tiles "
"WHERE region_id != ?1 "
+ " AND tile_id = id "
" AND url_template = ?2 "
" AND pixel_ratio = ?3 "
" AND x = ?4 "
@@ -969,7 +1026,10 @@ bool OfflineDatabase::markUsed(int64_t regionID, const Resource& resource) {
selectQuery.bind(4, tile.x);
selectQuery.bind(5, tile.y);
selectQuery.bind(6, tile.z);
- return !selectQuery.run();
+
+ bool notOnOtherRegion = !selectQuery.run();
+
+ return notOnThisRegion && notOnOtherRegion;
} else {
// clang-format off
mapbox::sqlite::Query insertQuery{ getStatement(
@@ -1083,7 +1143,7 @@ bool OfflineDatabase::evict(uint64_t neededFreeSize) {
// The addition of pageSize is a fudge factor to account for non `data` column
// size, and because pages can get fragmented on the database.
- while (usedSize() + neededFreeSize + pageSize > maximumCacheSize) {
+ while (usedSize() + neededFreeSize + pageSize > maximumAmbientCacheSize) {
// clang-format off
mapbox::sqlite::Query accessedQuery{ getStatement(
"SELECT max(accessed) "
@@ -1150,6 +1210,29 @@ bool OfflineDatabase::evict(uint64_t neededFreeSize) {
return true;
}
+std::exception_ptr OfflineDatabase::setMaximumAmbientCacheSize(uint64_t size) {
+ uint64_t previousMaximumAmbientCacheSize = maximumAmbientCacheSize;
+
+ try {
+ maximumAmbientCacheSize = size;
+
+ uint64_t databaseSize = getPragma<int64_t>("PRAGMA page_size")
+ * getPragma<int64_t>("PRAGMA page_count");
+
+ if (databaseSize > maximumAmbientCacheSize) {
+ evict(0);
+ db->exec("VACUUM");
+ }
+
+ return nullptr;
+ } catch (const mapbox::sqlite::Exception& ex) {
+ maximumAmbientCacheSize = previousMaximumAmbientCacheSize;
+ handleError(ex, "set maximum ambient cache size");
+
+ return std::current_exception();
+ }
+}
+
void OfflineDatabase::setOfflineMapboxTileCountLimit(uint64_t limit) {
offlineMapboxTileCountLimit = limit;
}
@@ -1195,7 +1278,7 @@ bool OfflineDatabase::exceedsOfflineMapboxTileCountLimit(const Resource& resourc
&& offlineMapboxTileCountLimitExceeded();
}
-std::exception_ptr OfflineDatabase::resetCache() try {
+std::exception_ptr OfflineDatabase::resetDatabase() try {
removeExisting();
initialize();
return nullptr;
diff --git a/platform/default/src/mbgl/util/format_number.cpp b/platform/default/src/mbgl/util/format_number.cpp
new file mode 100644
index 0000000000..7cc863818a
--- /dev/null
+++ b/platform/default/src/mbgl/util/format_number.cpp
@@ -0,0 +1,35 @@
+#include <mbgl/util/platform.hpp>
+
+#include <unicode/numberformatter.h>
+
+namespace mbgl {
+namespace platform {
+
+std::string formatNumber(double number, const std::string& localeId, const std::string& currency,
+ uint8_t minFractionDigits, uint8_t maxFractionDigits) {
+
+ UErrorCode status = U_ZERO_ERROR;
+ icu::UnicodeString ustr;
+ std::string formatted;
+
+ icu::Locale locale = icu::Locale(localeId.c_str());
+ // Print the value as currency
+ if (!currency.empty()) {
+ icu::UnicodeString ucurrency = icu::UnicodeString::fromUTF8(currency);
+ ustr = icu::number::NumberFormatter::with()
+ .unit(icu::CurrencyUnit(ucurrency.getBuffer(), status))
+ .locale(locale)
+ .formatDouble(number, status)
+ .toString();
+ } else {
+ ustr = icu::number::NumberFormatter::with()
+ .precision(icu::number::Precision::minMaxFraction(minFractionDigits, maxFractionDigits))
+ .locale(locale)
+ .formatDouble(number, status)
+ .toString();
+ }
+ return ustr.toUTF8String(formatted);
+}
+
+} // namespace platform
+} // namespace mbgl
diff --git a/platform/glfw/glfw_view.cpp b/platform/glfw/glfw_view.cpp
index cba3419672..41c3e93b35 100644
--- a/platform/glfw/glfw_view.cpp
+++ b/platform/glfw/glfw_view.cpp
@@ -222,7 +222,7 @@ void GLFWView::onKey(GLFWwindow *window, int key, int /*scancode*/, int action,
view->clearAnnotations();
break;
case GLFW_KEY_I:
- view->resetCacheCallback();
+ view->resetDatabaseCallback();
break;
case GLFW_KEY_K:
view->addRandomCustomPointAnnotations(1);
diff --git a/platform/glfw/glfw_view.hpp b/platform/glfw/glfw_view.hpp
index 3322932dde..9c9661546c 100644
--- a/platform/glfw/glfw_view.hpp
+++ b/platform/glfw/glfw_view.hpp
@@ -41,7 +41,7 @@ public:
}
void setResetCacheCallback(std::function<void()> callback) {
- resetCacheCallback = callback;
+ resetDatabaseCallback = callback;
};
void setShouldClose();
@@ -125,7 +125,7 @@ private:
std::function<void()> changeStyleCallback;
std::function<void()> pauseResumeCallback;
std::function<void()> onlineStatusCallback;
- std::function<void()> resetCacheCallback;
+ std::function<void()> resetDatabaseCallback;
std::function<void(mbgl::Map*)> animateRouteCallback;
mbgl::util::RunLoop runLoop;
diff --git a/platform/glfw/main.cpp b/platform/glfw/main.cpp
index a5001d1204..3c1e50e196 100644
--- a/platform/glfw/main.cpp
+++ b/platform/glfw/main.cpp
@@ -161,7 +161,7 @@ int main(int argc, char *argv[]) {
});
view->setResetCacheCallback([fileSource] () {
- fileSource->resetCache([](std::exception_ptr ex) {
+ fileSource->resetDatabase([](std::exception_ptr ex) {
if (ex) {
mbgl::Log::Error(mbgl::Event::Database, "Failed to reset cache:: %s", mbgl::util::toString(ex).c_str());
}
diff --git a/platform/ios/CHANGELOG.md b/platform/ios/CHANGELOG.md
index 46b69291e8..dc421fce22 100644
--- a/platform/ios/CHANGELOG.md
+++ b/platform/ios/CHANGELOG.md
@@ -4,22 +4,54 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT
## master
-* Fixed a crash caused by incorrect `MGLMapViewImpl` renderable size. ([#14810](https://github.com/mapbox/mapbox-gl-native/pull/14810))
+### Styles and rendering
+
+* Fixed style change transition regression caused by delayed setting of the updated layer properties. ([#15016](https://github.com/mapbox/mapbox-gl-native/pull/15016))
+
+### Other changes
+
* The MGLIdeographicFontFamilyName Info.plist key now also accepts an array of font family names, to customize font fallback behavior. It can also be set to a Boolean value of NO to force the SDK to typeset CJK characters in a remote font specified by MGLSymbolStyleLayer.textFontNames. ([#14862](https://github.com/mapbox/mapbox-gl-native/pull/14862))
-* Fixed queryRenderedFeatues bug caused by incorrect sort feature index calculation. ([#14884](https://github.com/mapbox/mapbox-gl-native/pull/14884))
-## 5.1.0
+## 5.2.0
+
+### Offline maps
+
+* Fixed an issue where offline regions could report the wrong number of tiles. ([#14958](https://github.com/mapbox/mapbox-gl-native/pull/14958))
+
+### Packaging
+
+* Removed previously deprecated methods and properties that had been marked `unavailable`. ([#15000](https://github.com/mapbox/mapbox-gl-native/pull/15000))
### Styles and rendering
-* Setting `MGLMapView.contentInset` now moves the map’s focal point to the center of the content frame after insetting. ([#14664](https://github.com/mapbox/mapbox-gl-native/pull/14664))
+* Added the `-[MGLMapViewDelegate mapView:shouldRemoveStyleImage:]` method for optimizing style image caching. ([#14769](https://github.com/mapbox/mapbox-gl-native/pull/14769))
+
+### Other changes
+
+* Added variants of several animated `MGLMapView` methods that accept completion handlers ([#14381](https://github.com/mapbox/mapbox-gl-native/pull/14381)):
+ * `-[MGLMapView setVisibleCoordinateBounds:edgePadding:animated:completionHandler:]`
+ * `-[MGLMapView setContentInset:animated:completionHandler:]`
+ * `-[MGLMapView setUserTrackingMode:animated:completionHandler:]`
+ * `-[MGLMapView setTargetCoordinate:animated:completionHandler:]`
+ * `-[MGLMapView showAnnotations:edgePadding:animated:completionHandler:]`
+ * `-[MGLMapView selectAnnotation:animated:completionHandler:]`
+* Deprecated variants of the above methods without completion handlers. ([#14959](https://github.com/mapbox/mapbox-gl-native/pull/14959))
+* Fixed an issue where the two-finger tilt gesture would continue after lifting one finger. ([#14969](https://github.com/mapbox/mapbox-gl-native/pull/14969))
+
+## 5.1.0 - June 19, 2019
+
+### Styles and rendering
+
+* Fixed a crash when a fill pattern in a style could not be found. ([#14696](https://github.com/mapbox/mapbox-gl-native/pull/14696))
* Fixed a rendering performance regression when rendering polylines. ([#14851](https://github.com/mapbox/mapbox-gl-native/pull/14851))
* Fixed a rendering performance regression introduced in 4.11.0. ([#14907](https://github.com/mapbox/mapbox-gl-native/pull/14907))
* Fixed an issue where symbols underneath opaque fill layers could be incorrectly drawn above such layers. ([#14839](https://github.com/mapbox/mapbox-gl-native/pull/14839))
+* Fixed an issue where `MGLFillExtrusionStyleLayer` vertical gradients might not be rendered. ([#14808](https://github.com/mapbox/mapbox-gl-native/pull/14808))
### Other changes
* The `-[MGLMapView setCamera:withDuration:animationTimingFunction:edgePadding:completionHandler:]` method now adds the current value of the `MGLMapView.contentInset` property to the `edgePadding` parameter. ([#14813](https://github.com/mapbox/mapbox-gl-native/pull/14813))
+* Setting `MGLMapView.contentInset` now moves the map’s focal point to the center of the content frame after insetting. ([#14664](https://github.com/mapbox/mapbox-gl-native/pull/14664))
* Fixed a feature querying bug caused by incorrect sort feature index calculation. ([#14884](https://github.com/mapbox/mapbox-gl-native/pull/14884))
## 5.0.0 - May 22, 2019
@@ -31,6 +63,7 @@ There are no breaking API changes in this release.
### Styles and rendering
* Changed placement order of `MGLSymbolStyleLayer` to match the viewport-y order when `MGLSymbolStyleLayer.symbolZOrder` is set to `MGLSymbolZOrderViewportY`, allowing icons to overlap but not text. ([#14486](https://github.com/mapbox/mapbox-gl-native/pull/14486))
+* Added `MGLSymbolStyleLayer.symbolSortKey` and `MGLSymbolZOrderAuto` to allow customization of symbol z-ordering. ([#14386](https://github.com/mapbox/mapbox-gl-native/pull/14386))
### Other changes
diff --git a/platform/ios/DEVELOPING.md b/platform/ios/DEVELOPING.md
index 5bf923b15b..eb0c705c29 100644
--- a/platform/ios/DEVELOPING.md
+++ b/platform/ios/DEVELOPING.md
@@ -140,11 +140,26 @@ To add an example code listing to the documentation for a class or class member:
[SourceKitten](https://github.com/jpsim/SourceKitten/) is required and will be installed automatically using Homebrew.
+### Customizing compilation settings
+
+You can provide an optional and custom [`xcconfig`](https://help.apple.com/xcode/mac/current/#/dev745c5c974) file named `platform/darwin/developer.xcconfig` to set custom build options. This file is ignored by git. These custom settings apply to all configurations (`Debug`, `Release`, `RelWithDebInfo`), but do **not** apply to the core `mbgl` files. This mechanism allows you to try different compiler settings (for example when testing an Xcode beta).
+
## Testing
-`make ios-test` builds and runs unit tests of cross-platform code as well as the SDK.
+`make ios-test` builds and runs unit tests of cross-platform code and of the SDK. Other types of tests available include:
+
+* `make ios-integration-test` runs UI tests from the "Integration Test Harness" scheme.
+* `make ios-sanitize` runs unit tests from the "CI" scheme with the Thread Sanitizer and Undefined Behavior Sanitizer enabled.
+* `make ios-sanitize-address` runs unit tests from the "CI" scheme with the Address Sanitizer enabled.
+* `make ios-static-analyzer` runs unit tests from the "CI" scheme with the Static Analyzer enabled.
+
+These commands are run by default on a single Simulator. To enable legacy iOS versions and more device types, add `MORE_SIMULATORS=YES`. Use `IOS_LATEST=YES`, `IOS_11=YES`, etc. to test on specific iOS versions.
+
+To only run a specific test or class of tests, add `ONLY_TESTING=test/MGLNameOfTestClass/testNameOfTest`.
+
+To skip a specific test or class of tests, add `SKIP_TESTING=test/MGLNameOfTestClass/testNameOfTest`.
-To instead run the cross-platform tests in Xcode instead of on the command line:
+To run the cross-platform tests in Xcode instead of on the command line:
1. Run `make iproj` to set up the workspace.
1. Change the scheme to “test (platform project)” and press Command-R to run core unit tests.
diff --git a/platform/ios/Integration Tests/Annotation Tests/MGLAnnotationViewIntegrationTests.m b/platform/ios/Integration Tests/Annotation Tests/MGLAnnotationViewIntegrationTests.m
index 0b32df55b4..162bfc5237 100644
--- a/platform/ios/Integration Tests/Annotation Tests/MGLAnnotationViewIntegrationTests.m
+++ b/platform/ios/Integration Tests/Annotation Tests/MGLAnnotationViewIntegrationTests.m
@@ -41,7 +41,7 @@ typedef struct PanTestData {
} PanTestData;
#define PAN_TEST_TERMINATOR {{FLT_MAX, FLT_MAX}, NO, NO, NO, NO, NO}
-static const CGFloat kAnnotationScale = 0.125f;
+static const CGPoint kAnnotationRelativeScale = { 0.05f, 0.125f };
- (void)internalTestOffscreenSelectionTitle:(NSString*)title withTestData:(PanTestData)test animateSelection:(BOOL)animateSelection {
@@ -61,7 +61,7 @@ static const CGFloat kAnnotationScale = 0.125f;
NSString * const MGLTestAnnotationReuseIdentifer = @"MGLTestAnnotationReuseIdentifer";
CGSize size = self.mapView.bounds.size;
- CGSize annotationSize = CGSizeMake(floor(size.width*kAnnotationScale), floor(size.height*kAnnotationScale));
+ CGSize annotationSize = CGSizeMake(floor(size.width*kAnnotationRelativeScale.x), floor(size.height*kAnnotationRelativeScale.y));
self.viewForAnnotation = ^MGLAnnotationView*(MGLMapView *view, id<MGLAnnotation> annotation) {
@@ -79,7 +79,7 @@ static const CGFloat kAnnotationScale = 0.125f;
};
// Coordinate for annotation screen coordinate
- CGPoint annotationPoint = CGPointMake(relativeCoordinate.x * size.width, relativeCoordinate.y * size.height);
+ CGPoint annotationPoint = CGPointMake(floor(relativeCoordinate.x * size.width), floor(relativeCoordinate.y * size.height) );
CLLocationCoordinate2D coordinate = [self.mapView convertPoint:annotationPoint toCoordinateFromView:self.mapView];
MGLPointAnnotation *point = [[MGLPointAnnotation alloc] init];
@@ -112,13 +112,16 @@ static const CGFloat kAnnotationScale = 0.125f;
// Also note, that at this point, the internal mechanism that determines if
// an annotation view is offscreen and should be put back in the reuse queue
// will have run, and `viewForAnnotation` may return nil
-
- [self.mapView selectAnnotation:point moveIntoView:moveIntoView animateSelection:animateSelection];
+
+ XCTestExpectation *selectionCompleted = [self expectationWithDescription:@"Selection completed"];
+ [self.mapView selectAnnotation:point moveIntoView:moveIntoView animateSelection:animateSelection completionHandler:^{
+ [selectionCompleted fulfill];
+ }];
// Animated selection takes MGLAnimationDuration (0.3 seconds), so wait a little
// longer. We don't need to wait as long if we're not animated (but we do
// want the runloop to tick over)
- [self waitFor:animateSelection ? 0.4: 0.05];
+ [self waitForExpectations:@[selectionCompleted] timeout:animateSelection ? 0.4: 0.05];
UIView *annotationViewAfterSelection = [self.mapView viewForAnnotation:point];
CLLocationCoordinate2D mapCenterAfterSelection = self.mapView.centerCoordinate;
@@ -128,7 +131,7 @@ static const CGFloat kAnnotationScale = 0.125f;
// may be nil, which is expected.
BOOL (^CGRectContainsRectWithAccuracy)(CGRect, CGRect, CGFloat) = ^(CGRect rect1, CGRect rect2, CGFloat accuracy) {
CGRect expandedRect1 = CGRectInset(rect1, -accuracy, -accuracy);
- return CGRectContainsRect(expandedRect1, rect2);
+ return (BOOL)CGRectContainsRect(expandedRect1, rect2);
};
CGFloat epsilon = 0.00001;
@@ -151,9 +154,8 @@ static const CGFloat kAnnotationScale = 0.125f;
UIView *calloutView = self.mapView.calloutViewForSelectedAnnotation;
XCTAssertNotNil(calloutView);
- // If kAnnotationScale == 0.25, then the following assert can fail.
- // This is really a warning (see https://github.com/mapbox/mapbox-gl-native/issues/13744 )
- // If you need this NOT to fail the tests, consider replacing with MGLTestWarning
+ // This can fail if the callout view's width is < the annotations. This is really a warning, so
+ // if you need this NOT to fail the tests, consider replacing with MGLTestWarning
XCTAssert(expectCalloutToBeFullyOnscreen == CGRectContainsRectWithAccuracy(self.mapView.bounds, calloutView.frame, 0.25),
@"Expect contains:%d, Mapview:%@ annotation:%@ callout:%@",
expectCalloutToBeFullyOnscreen,
@@ -320,6 +322,8 @@ static const CGFloat kAnnotationScale = 0.125f;
// | Onscreen | Yes | Yes | Yes, but *only* to ensure callout is fully visible |
//
+ CGFloat offset = kAnnotationRelativeScale.x * 0.5f;
+
PanTestData tests[] = {
// Coord showsCallout impl margins? moveIntoView expectMapToPan calloutOnScreen
// Offscreen
@@ -341,9 +345,10 @@ static const CGFloat kAnnotationScale = 0.125f;
// Expects to move, because although onscreen, callout would not be.
// However, if the scale is 0.25, then expectToPan should be NO, because
// of the width of the annotation
- //
- // Coord showsCallout impl margins? moveIntoView expectMapToPan calloutOnScreen
- { {kAnnotationScale, 0.5f}, YES, YES, YES, (kAnnotationScale == 0.125f), YES },
+
+ // Coord showsCallout impl margins? moveIntoView expectMapToPan calloutOnScreen
+ { {offset, 0.5f}, YES, YES, YES, YES, YES },
+ { {1.0 - offset, 0.5f}, YES, YES, YES, YES, YES },
PAN_TEST_TERMINATOR
};
@@ -475,12 +480,12 @@ static const CGFloat kAnnotationScale = 0.125f;
MGLTestLocationManager *locationManager = [[MGLTestLocationManager alloc] init];
self.mapView.locationManager = locationManager;
- [self.mapView setUserTrackingMode:MGLUserTrackingModeFollow animated:NO];
+ [self.mapView setUserTrackingMode:MGLUserTrackingModeFollow animated:NO completionHandler:nil];
CGRect originalFrame = [self.mapView viewForAnnotation:self.mapView.userLocation].frame;
// Temporarily disable location tracking so we can save the value of
// the originalFrame in memory
- [self.mapView setUserTrackingMode:MGLUserTrackingModeNone animated:NO];
+ [self.mapView setUserTrackingMode:MGLUserTrackingModeNone animated:NO completionHandler:nil];
CGPoint offset = CGPointMake(20, 20);
@@ -488,7 +493,7 @@ static const CGFloat kAnnotationScale = 0.125f;
return offset;;
};
- [self.mapView setUserTrackingMode:MGLUserTrackingModeFollow animated:NO];
+ [self.mapView setUserTrackingMode:MGLUserTrackingModeFollow animated:NO completionHandler:nil];
CGRect offsetFrame = [self.mapView viewForAnnotation:self.mapView.userLocation].frame;
XCTAssertEqual(originalFrame.origin.x + offset.x, offsetFrame.origin.x);
diff --git a/platform/ios/Integration Tests/MGLCameraTransitionTests.mm b/platform/ios/Integration Tests/MGLCameraTransitionTests.mm
index 60d5fc6c9a..9679c4c11f 100644
--- a/platform/ios/Integration Tests/MGLCameraTransitionTests.mm
+++ b/platform/ios/Integration Tests/MGLCameraTransitionTests.mm
@@ -328,7 +328,7 @@
NSLog(@"setCenterCoordinate: %0.4fs", stop1 - stop0);
NSLog(@"flyToCamera: %0.4fs", stop2 - stop1);
- XCTAssert(delegateCallCount == 2, @"Expecting 2 regionDidChange callbacks, got %ld", delegateCallCount); // Once for the setDirection and once for the reset north
+ XCTAssert(delegateCallCount == 2, @"Expecting 2 regionDidChange callbacks, got %ld", (long)delegateCallCount); // Once for the setDirection and once for the reset north
}
#pragma mark - Pending tests
diff --git a/platform/ios/Mapbox-iOS-SDK-snapshot-dynamic.podspec b/platform/ios/Mapbox-iOS-SDK-snapshot-dynamic.podspec
index 0fe9c4ad2c..ad2df155ec 100644
--- a/platform/ios/Mapbox-iOS-SDK-snapshot-dynamic.podspec
+++ b/platform/ios/Mapbox-iOS-SDK-snapshot-dynamic.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |m|
- version = '5.1.0-beta.1'
+ version = '5.2.0-alpha.1'
m.name = 'Mapbox-iOS-SDK-snapshot-dynamic'
m.version = "#{version}-snapshot"
diff --git a/platform/ios/Mapbox-iOS-SDK-static-part.podspec b/platform/ios/Mapbox-iOS-SDK-static-part.podspec
deleted file mode 100644
index b82c2bb24f..0000000000
--- a/platform/ios/Mapbox-iOS-SDK-static-part.podspec
+++ /dev/null
@@ -1,14 +0,0 @@
- m.ios.deployment_target = '9.0'
-
- m.requires_arc = true
-
- m.preserve_paths = '**'
- m.source_files = 'Headers/*.h', 'MGLDummy.m'
- m.resource_bundle = { 'Mapbox' => 'Mapbox.bundle/*' }
- m.vendored_frameworks = 'Mapbox.framework'
- m.module_name = 'Mapbox'
- m.xcconfig = { 'OTHER_LDFLAGS' => '-ObjC' }
-
- m.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
-
-end
diff --git a/platform/ios/Mapbox-iOS-SDK-stripped.podspec b/platform/ios/Mapbox-iOS-SDK-stripped.podspec
index 33a0e3bdcb..bdc795fe75 100644
--- a/platform/ios/Mapbox-iOS-SDK-stripped.podspec
+++ b/platform/ios/Mapbox-iOS-SDK-stripped.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |m|
- version = '5.1.0-beta.1'
+ version = '5.2.0-alpha.1'
m.name = 'Mapbox-iOS-SDK-stripped'
m.version = "#{version}-stripped"
diff --git a/platform/ios/Mapbox-iOS-SDK.podspec b/platform/ios/Mapbox-iOS-SDK.podspec
index af37016491..b0d02d14df 100644
--- a/platform/ios/Mapbox-iOS-SDK.podspec
+++ b/platform/ios/Mapbox-iOS-SDK.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |m|
- version = '5.1.0-beta.1'
+ version = '5.2.0-alpha.1'
m.name = 'Mapbox-iOS-SDK'
m.version = version
diff --git a/platform/ios/app/MBXState.m b/platform/ios/app/MBXState.m
index 49ccd67e8d..0365306637 100644
--- a/platform/ios/app/MBXState.m
+++ b/platform/ios/app/MBXState.m
@@ -21,9 +21,9 @@ NSString *const MBXReuseQueueStatsEnabled = @"MBXReuseQueueStatsEnabled";
- (void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeObject:_camera forKey:MBXCamera];
- [coder encodeObject:[NSNumber numberWithInt:_userTrackingMode] forKey:MBXUserTrackingMode];
+ [coder encodeObject:[NSNumber numberWithUnsignedInteger:_userTrackingMode] forKey:MBXUserTrackingMode];
[coder encodeBool:_showsUserLocation forKey:MBXShowsUserLocation];
- [coder encodeObject:[NSNumber numberWithInt:_debugMask] forKey:MBXDebugMaskValue];
+ [coder encodeObject:[NSNumber numberWithUnsignedInteger:_debugMask] forKey:MBXDebugMaskValue];
[coder encodeBool:_showsZoomLevelOrnament forKey:MBXShowsZoomLevelOrnament];
[coder encodeBool:_showsTimeFrameGraph forKey:MBXShowsTimeFrameGraph];
[coder encodeBool:_debugLoggingEnabled forKey:MBXDebugLoggingEnabled];
@@ -68,7 +68,18 @@ NSString *const MBXReuseQueueStatsEnabled = @"MBXReuseQueueStatsEnabled";
}
- (NSString*) debugDescription {
- return [NSString stringWithFormat:@"Camera: %@\nTracking mode: %lu\nShows user location: %@\nDebug mask value: %lu\nShows zoom level ornament: %@\nShows time frame graph: %@\nDebug logging enabled: %@\nShows map scale: %@\nShows user heading indicator: %@\nFramerate measurement enabled: %@", self.camera, (unsigned long)self.userTrackingMode, (self.showsUserLocation) ? @"YES" : @"NO", (unsigned long)self.debugMask, (self.showsZoomLevelOrnament) ? @"YES" : @"NO", (self.showsTimeFrameGraph) ? @"YES" : @"NO", (self.debugLoggingEnabled) ? @"YES" : @"NO", (self.showsMapScale) ? @"YES" : @"NO", (self.showsUserHeadingIndicator) ? @"YES" : @"NO", (self.framerateMeasurementEnabled) ? @"YES" : @"NO", (self.reuseQueueStatsEnabled) ? @"YES" : @"NO"];
+ return [NSString stringWithFormat:@"Camera: %@\nTracking mode: %lu\nShows user location: %@\nDebug mask value: %lu\nShows zoom level ornament: %@\nShows time frame graph: %@\nDebug logging enabled: %@\nShows map scale: %@\nShows user heading indicator: %@\nFramerate measurement enabled: %@\nReuse queue stats enabled: %@",
+ self.camera,
+ (unsigned long)self.userTrackingMode,
+ self.showsUserLocation ? @"YES" : @"NO",
+ (unsigned long)self.debugMask,
+ self.showsZoomLevelOrnament ? @"YES" : @"NO",
+ self.showsTimeFrameGraph ? @"YES" : @"NO",
+ self.debugLoggingEnabled ? @"YES" : @"NO",
+ self.showsMapScale ? @"YES" : @"NO",
+ self.showsUserHeadingIndicator ? @"YES" : @"NO",
+ self.framerateMeasurementEnabled ? @"YES" : @"NO",
+ self.reuseQueueStatsEnabled ? @"YES" : @"NO"];
}
@end
diff --git a/platform/ios/app/MBXStateManager.m b/platform/ios/app/MBXStateManager.m
index 679a4a075f..7203ae462d 100644
--- a/platform/ios/app/MBXStateManager.m
+++ b/platform/ios/app/MBXStateManager.m
@@ -3,6 +3,8 @@
#import "MBXState.h"
#import "MBXViewController.h"
+static NSString * const MBXStateManagerMapStateKey = @"mapStateKey";
+
@interface MBXStateManager()
@property (strong, nonatomic) MBXState *currentState;
@@ -22,20 +24,19 @@
}
- (MBXState*)currentState {
- NSData *encodedMapState = [[NSUserDefaults standardUserDefaults] objectForKey:@"mapStateKey"];
- MBXState *currentState = (MBXState *)[NSKeyedUnarchiver unarchiveObjectWithData: encodedMapState];
+ NSData *encodedMapState = [[NSUserDefaults standardUserDefaults] objectForKey:MBXStateManagerMapStateKey];
+ MBXState *currentState = (MBXState *)[NSKeyedUnarchiver unarchiveObjectWithData:encodedMapState];
return currentState;
}
- (void)saveState:(MBXState*)mapState {
NSData *encodedMapState = [NSKeyedArchiver archivedDataWithRootObject:mapState];
- [[NSUserDefaults standardUserDefaults] setObject:encodedMapState forKey:@"mapStateKey"];
- [[NSUserDefaults standardUserDefaults] synchronize];
+ [[NSUserDefaults standardUserDefaults] setObject:encodedMapState forKey:MBXStateManagerMapStateKey];
}
- (void)resetState {
- [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"mapStateKey"];
+ [[NSUserDefaults standardUserDefaults] removeObjectForKey:MBXStateManagerMapStateKey];
}
diff --git a/platform/ios/app/MBXViewController.m b/platform/ios/app/MBXViewController.m
index 44b8ae2bac..2fb95e1b17 100644
--- a/platform/ios/app/MBXViewController.m
+++ b/platform/ios/app/MBXViewController.m
@@ -751,14 +751,14 @@ CLLocationCoordinate2D randomWorldCoordinate() {
heading:0];
__weak typeof(self) weakSelf = self;
[self.mapView setCamera:camera withDuration:0.3 animationTimingFunction:nil completionHandler:^{
- [weakSelf.mapView setContentInset:contentInsets animated:YES];
+ [weakSelf.mapView setContentInset:contentInsets animated:YES completionHandler:nil];
}];
} else {
[self.view sendSubviewToBack:self.contentInsetsOverlays[0]];
[self.view sendSubviewToBack:self.contentInsetsOverlays[1]];
[self.view sendSubviewToBack:self.contentInsetsOverlays[2]];
[self.view sendSubviewToBack:self.contentInsetsOverlays[3]];
- [self.mapView setContentInset:_originalContentInsets animated:YES];
+ [self.mapView setContentInset:_originalContentInsets animated:YES completionHandler:nil];
}
break;
}
@@ -1686,7 +1686,7 @@ CLLocationCoordinate2D randomWorldCoordinate() {
- (void)selectAnOffscreenPointAnnotation {
id<MGLAnnotation> annotation = [self randomOffscreenPointAnnotation];
if (annotation) {
- [self.mapView selectAnnotation:annotation animated:YES];
+ [self.mapView selectAnnotation:annotation animated:YES completionHandler:nil];
NSAssert(self.mapView.selectedAnnotations.firstObject, @"The annotation was not selected");
}
@@ -1946,7 +1946,7 @@ CLLocationCoordinate2D randomWorldCoordinate() {
// positioning rect)
[self.mapView addAnnotation:pin];
- [self.mapView selectAnnotation:pin animated:YES];
+ [self.mapView selectAnnotation:pin animated:YES completionHandler:nil];
}
}
diff --git a/platform/ios/ios.xcodeproj/project.pbxproj b/platform/ios/ios.xcodeproj/project.pbxproj
index 8cb261c64b..7c2935fc59 100644
--- a/platform/ios/ios.xcodeproj/project.pbxproj
+++ b/platform/ios/ios.xcodeproj/project.pbxproj
@@ -3867,6 +3867,7 @@
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
@@ -4145,6 +4146,7 @@
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
@@ -4217,6 +4219,7 @@
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
diff --git a/platform/ios/scripts/lint-podspecs.js b/platform/ios/scripts/lint-podspecs.js
new file mode 100755
index 0000000000..1381acacff
--- /dev/null
+++ b/platform/ios/scripts/lint-podspecs.js
@@ -0,0 +1,79 @@
+#!/usr/bin/env node
+
+const fs = require('fs');
+const execSync = require('child_process').execSync;
+const _ = require('lodash');
+const semver = require('semver');
+
+console.step = _.partial(console.log, '\n\033[1m\033[36m*', _, '\033[0m');
+process.exitCode = 0;
+
+const podspecsPath = 'platform/ios';
+
+/*
+ Step 1. Run the official CocoaPods linter against all of our podspecs.
+*/
+console.step(`Running CocoaPods linter against podspecs in '${podspecsPath}'`)
+execSync(`pod spec lint ${podspecsPath}/*.podspec --quick`, {stdio: 'inherit'});
+
+/*
+ Step 2. Check that our podspecs contain properly formatted version strings.
+*/
+console.step('Checking Mapbox SDK version strings in podspecs')
+const podspecs = fs.readdirSync(podspecsPath).filter(fn => fn.endsWith('.podspec'));
+
+let matchedVersions = [];
+
+for (const podspecFilename of podspecs) {
+ console.log(podspecFilename);
+ const podspecContents = fs.readFileSync(`${podspecsPath}/${podspecFilename}`, 'utf8');
+
+ /*
+ This regular expression:
+ - Matches single lines in the format: version = '9.9.9'
+ - Groups the version number inside the single quotes.
+ - Ignores whitespace at the start of the line and between parts of the definition using: \s*?
+ */
+ const regex = /^\s*?version\s*?=\s*?'(.*)'$/gmi;
+
+ const match = regex.exec(podspecContents);
+ if (!match) {
+ console.error(' ❌ No version string found');
+ process.exitCode = 1;
+ continue;
+ }
+
+ const matchedVersion = match[1];
+ matchedVersions.push(matchedVersion);
+
+ if (!semver.valid(matchedVersion)) {
+ console.error(` ❌ ${matchedVersion} is not a valid semantic version`);
+ process.exitCode = 1;
+ continue;
+ }
+
+ console.log(` ✅ ${matchedVersion} is a valid semantic version`);
+}
+
+/*
+ Step 3. Check that versions are consistent across our podspecs.
+*/
+console.step('Checking version consistency')
+const uniqueVersions = _.uniq(matchedVersions);
+if (uniqueVersions.length != 1) {
+ console.error('❌ Versions in podspecs do not match:', uniqueVersions);
+ process.exitCode = 1;
+} else {
+ console.log(`✅ ${uniqueVersions[0]} is set in all podspecs`);
+}
+
+/*
+ Step 4. Finish up and summarize the results.
+*/
+process.on('exit', (code) => {
+ if (code == 0) {
+ console.log('\nPodspec linting completed successfully! 🕵️‍♀️');
+ } else {
+ console.log('\nPodspec linting failed. 💥');
+ }
+});
diff --git a/platform/ios/scripts/package.sh b/platform/ios/scripts/package.sh
index a37b3c7fda..5475632c24 100755
--- a/platform/ios/scripts/package.sh
+++ b/platform/ios/scripts/package.sh
@@ -206,34 +206,6 @@ if [[ ${BUILD_DYNAMIC} == true && ${BUILDTYPE} == Release ]]; then
lipo -info "${OUTPUT}/dynamic/${NAME}.framework.dSYM/Contents/Resources/DWARF/${NAME}"
fi
-function create_podspec {
- step "Creating local podspec (${1})"
- [[ $SYMBOLS = NO ]] && POD_SUFFIX="-stripped" || POD_SUFFIX=""
- POD_SOURCE_PATH=' :path => ".",'
- POD_FRAMEWORKS=" m.vendored_frameworks = '"${NAME}".framework'"
- INPUT_PODSPEC=platform/ios/${NAME}-iOS-SDK${POD_SUFFIX}.podspec
- OUTPUT_PODSPEC=${OUTPUT}/${1}/${NAME}-iOS-SDK${POD_SUFFIX}.podspec
- if [[ ${1} == "dynamic" ]]; then
- sed "s/.*:http.*/${POD_SOURCE_PATH}/" ${INPUT_PODSPEC} > ${OUTPUT_PODSPEC}
- sed -i '' "s/.*vendored_frameworks.*/${POD_FRAMEWORKS}/" ${OUTPUT_PODSPEC}
- fi
- if [[ ${1} == "static" ]]; then
- awk '/Pod::Spec.new/,/m.platform/' ${INPUT_PODSPEC} > ${OUTPUT_PODSPEC}
- cat platform/ios/${NAME}-iOS-SDK-static-part.podspec >> ${OUTPUT_PODSPEC}
- sed -i '' "s/.*:http.*/${POD_SOURCE_PATH}/" ${OUTPUT_PODSPEC}
- fi
- cp -pv LICENSE.md ${OUTPUT}/${1}/
-}
-
-if [[ ${BUILD_STATIC} == true ]]; then
- stat "${OUTPUT}/static/${NAME}.framework"
- create_podspec "static"
-fi
-if [[ ${BUILD_DYNAMIC} == true ]]; then
- stat "${OUTPUT}/dynamic/${NAME}.framework"
- create_podspec "dynamic"
-fi
-
if [[ ${BUILD_STATIC} == true ]]; then
step "Copying static library headers…"
cp -rv "${PRODUCTS}/${BUILDTYPE}-iphoneos/Headers" "${OUTPUT}/static/${NAME}.framework/Headers"
diff --git a/platform/ios/src/MGLMapView+Impl.h b/platform/ios/src/MGLMapView+Impl.h
index 3a7488b443..0a62b7da82 100644
--- a/platform/ios/src/MGLMapView+Impl.h
+++ b/platform/ios/src/MGLMapView+Impl.h
@@ -69,6 +69,7 @@ public:
void onSourceChanged(mbgl::style::Source& source) override;
void onDidBecomeIdle() override;
void onStyleImageMissing(const std::string& imageIdentifier) override;
+ bool onCanRemoveUnusedStyleImage(const std::string& imageIdentifier) override;
protected:
/// Cocoa map view that this adapter bridges to.
diff --git a/platform/ios/src/MGLMapView+Impl.mm b/platform/ios/src/MGLMapView+Impl.mm
index 73e692defe..1bccfa662f 100644
--- a/platform/ios/src/MGLMapView+Impl.mm
+++ b/platform/ios/src/MGLMapView+Impl.mm
@@ -100,3 +100,8 @@ void MGLMapViewImpl::onStyleImageMissing(const std::string& imageIdentifier) {
NSString *imageName = [NSString stringWithUTF8String:imageIdentifier.c_str()];
[mapView didFailToLoadImage:imageName];
}
+
+bool MGLMapViewImpl::onCanRemoveUnusedStyleImage(const std::string &imageIdentifier) {
+ NSString *imageName = [NSString stringWithUTF8String:imageIdentifier.c_str()];
+ return [mapView shouldRemoveStyleImage:imageName];
+}
diff --git a/platform/ios/src/MGLMapView.h b/platform/ios/src/MGLMapView.h
index 3baae2ee3c..87c24cd97b 100644
--- a/platform/ios/src/MGLMapView.h
+++ b/platform/ios/src/MGLMapView.h
@@ -253,8 +253,6 @@ MGL_EXPORT
*/
@property (nonatomic, readonly, nullable) MGLStyle *style;
-@property (nonatomic, readonly) NSArray<NSURL *> *bundledStyleURLs __attribute__((unavailable("Call the relevant class method of MGLStyle for the URL of a particular default style.")));
-
/**
URL of the style currently displayed in the receiver.
@@ -423,14 +421,6 @@ MGL_EXPORT
*/
@property (nonatomic, assign) BOOL prefetchesTiles;
-@property (nonatomic) NSArray<NSString *> *styleClasses __attribute__((unavailable("Support for style classes has been removed.")));
-
-- (BOOL)hasStyleClass:(NSString *)styleClass __attribute__((unavailable("Support for style classes has been removed.")));
-
-- (void)addStyleClass:(NSString *)styleClass __attribute__((unavailable("Support for style classes has been removed.")));
-
-- (void)removeStyleClass:(NSString *)styleClass __attribute__((unavailable("Support for style classes has been removed.")));
-
#pragma mark Displaying the User’s Location
/**
@@ -503,7 +493,10 @@ MGL_EXPORT
@property (nonatomic, assign) MGLUserTrackingMode userTrackingMode;
/**
- Sets the mode used to track the user location, with an optional transition.
+ Deprecated. Sets the mode used to track the user location, with an optional transition.
+
+ To specify a completion handler to execute after the animation finishes, use
+ the `-setUserTrackingMode:animated:completionHandler:` method.
@param mode The mode used to track the user location.
@param animated If `YES`, there is an animated transition from the current
@@ -512,7 +505,21 @@ MGL_EXPORT
affects the initial transition; subsequent changes to the user location or
heading are always animated.
*/
-- (void)setUserTrackingMode:(MGLUserTrackingMode)mode animated:(BOOL)animated;
+- (void)setUserTrackingMode:(MGLUserTrackingMode)mode animated:(BOOL)animated __attribute__((deprecated("Use `-setUserTrackingMode:animated:completionHandler:` instead.")));
+
+/**
+ Sets the mode used to track the user location, with an optional transition and
+ completion handler.
+
+ @param mode The mode used to track the user location.
+ @param animated If `YES`, there is an animated transition from the current
+ viewport to a viewport that results from the change to `mode`. If `NO`, the
+ map view instantaneously changes to the new viewport. This parameter only
+ affects the initial transition; subsequent changes to the user location or
+ heading are always animated.
+ @param completion The block executed after the animation finishes.
+ */
+- (void)setUserTrackingMode:(MGLUserTrackingMode)mode animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion;
/**
The vertical alignment of the user location annotation within the receiver. The
@@ -592,8 +599,32 @@ MGL_EXPORT
@property (nonatomic, assign) CLLocationCoordinate2D targetCoordinate;
/**
+ Deprecated. Sets the geographic coordinate that is the subject of observation as
+ the user location is being tracked, with an optional transition animation.
+
+ By default, the target coordinate is set to an invalid coordinate, indicating
+ that there is no target. In course tracking mode, the target forms one of two
+ foci in the viewport, the other being the user location annotation. Typically,
+ the target is set to a destination or waypoint in a real-time navigation scene.
+ As the user annotation moves toward the target, the map automatically zooms in
+ to fit both foci optimally within the viewport.
+
+ This method has no effect if the `userTrackingMode` property is set to a value
+ other than `MGLUserTrackingModeFollowWithCourse`.
+
+ To specify a completion handler to execute after the animation finishes, use
+ the `-setTargetCoordinate:animated:completionHandler:` method.
+
+ @param targetCoordinate The target coordinate to fit within the viewport.
+ @param animated If `YES`, the map animates to fit the target within the map
+ view. If `NO`, the map fits the target instantaneously.
+ */
+- (void)setTargetCoordinate:(CLLocationCoordinate2D)targetCoordinate animated:(BOOL)animated __attribute__((deprecated("Use `-setTargetCoordinate:animated:completionHandler:` instead.")));
+
+/**
Sets the geographic coordinate that is the subject of observation as the user
- location is being tracked, with an optional transition animation.
+ location is being tracked, with an optional transition animation and completion
+ handler.
By default, the target coordinate is set to an invalid coordinate, indicating
that there is no target. In course tracking mode, the target forms one of two
@@ -608,8 +639,9 @@ MGL_EXPORT
@param targetCoordinate The target coordinate to fit within the viewport.
@param animated If `YES`, the map animates to fit the target within the map
view. If `NO`, the map fits the target instantaneously.
+ @param completion The block executed after the animation finishes.
*/
-- (void)setTargetCoordinate:(CLLocationCoordinate2D)targetCoordinate animated:(BOOL)animated;
+- (void)setTargetCoordinate:(CLLocationCoordinate2D)targetCoordinate animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion;
#pragma mark Configuring How the User Interacts with the Map
@@ -892,8 +924,28 @@ MGL_EXPORT
- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds animated:(BOOL)animated;
/**
- Changes the receiver’s viewport to fit the given coordinate bounds and
- optionally some additional padding on each side.
+ Deprecated. Changes the receiver’s viewport to fit the given coordinate bounds with
+ some additional padding on each side.
+
+ To bring both sides of the antimeridian or international date line into view,
+ specify some longitudes less than −180 degrees or greater than 180 degrees. For
+ example, to show both Tokyo and San Francisco simultaneously, you could set the
+ visible bounds to extend from (35.68476, −220.24257) to (37.78428, −122.41310).
+
+ To specify a completion handler to execute after the animation finishes, use
+ the `-setVisibleCoordinateBounds:edgePadding:animated:completionHandler:` method.
+
+ @param bounds The bounds that the viewport will show in its entirety.
+ @param insets The minimum padding (in screen points) that will be visible
+ around the given coordinate bounds.
+ @param animated Specify `YES` to animate the change by smoothly scrolling and
+ zooming or `NO` to immediately display the given bounds.
+ */
+- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated __attribute__((deprecated("Use `-setVisibleCoordinateBounds:edgePadding:animated:completionHandler:` instead.")));
+
+/**
+ Changes the receiver’s viewport to fit the given coordinate bounds with some
+ additional padding on each side, optionally calling a completion handler.
To bring both sides of the antimeridian or international date line into view,
specify some longitudes less than −180 degrees or greater than 180 degrees. For
@@ -905,12 +957,13 @@ MGL_EXPORT
around the given coordinate bounds.
@param animated Specify `YES` to animate the change by smoothly scrolling and
zooming or `NO` to immediately display the given bounds.
+ @param completion The block executed after the animation finishes.
*/
-- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated;
+- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion;
/**
- Changes the receiver’s viewport to fit all of the given coordinates and
- optionally some additional padding on each side.
+ Changes the receiver’s viewport to fit all of the given coordinates with some
+ additional padding on each side.
To bring both sides of the antimeridian or international date line into view,
specify some longitudes less than −180 degrees or greater than 180 degrees. For
@@ -928,8 +981,8 @@ MGL_EXPORT
- (void)setVisibleCoordinates:(const CLLocationCoordinate2D *)coordinates count:(NSUInteger)count edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated;
/**
- Changes the receiver’s viewport to fit all of the given coordinates and
- optionally some additional padding on each side.
+ Changes the receiver’s viewport to fit all of the given coordinates with some
+ additional padding on each side, optionally calling a completion handler.
To bring both sides of the antimeridian or international date line into view,
specify some longitudes less than −180 degrees or greater than 180 degrees. For
@@ -965,8 +1018,27 @@ MGL_EXPORT
- (void)showAnnotations:(NSArray<id <MGLAnnotation>> *)annotations animated:(BOOL)animated;
/**
+ Deprecated. Sets the visible region so that the map displays the specified
+ annotations with the specified amount of padding on each side.
+
+ Calling this method updates the value in the `visibleCoordinateBounds` property
+ and potentially other properties to reflect the new map region.
+
+ To specify a completion handler to execute after the animation finishes, use
+ the `-showAnnotations:edgePadding:animated:completionHandler:` method.
+
+ @param annotations The annotations that you want to be visible in the map.
+ @param insets The minimum padding (in screen points) around the edges of the
+ map view to keep clear of annotations.
+ @param animated `YES` if you want the map region change to be animated, or `NO`
+ if you want the map to display the new region immediately without animations.
+ */
+- (void)showAnnotations:(NSArray<id <MGLAnnotation>> *)annotations edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated __attribute__((deprecated("Use `-showAnnotations:edgePadding:animated:completionHandler:` instead.")));
+
+/**
Sets the visible region so that the map displays the specified annotations with
- the specified amount of padding on each side.
+ the specified amount of padding on each side and an optional completion
+ handler.
Calling this method updates the value in the `visibleCoordinateBounds` property
and potentially other properties to reflect the new map region.
@@ -976,8 +1048,9 @@ MGL_EXPORT
map view to keep clear of annotations.
@param animated `YES` if you want the map region change to be animated, or `NO`
if you want the map to display the new region immediately without animations.
+ @param completion The block executed after the animation finishes.
*/
-- (void)showAnnotations:(NSArray<id <MGLAnnotation>> *)annotations edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated;
+- (void)showAnnotations:(NSArray<id <MGLAnnotation>> *)annotations edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion;
/**
A camera representing the current viewpoint of the map.
@@ -1117,8 +1190,8 @@ MGL_EXPORT
- (MGLMapCamera *)cameraThatFitsCoordinateBounds:(MGLCoordinateBounds)bounds;
/**
- Returns the camera that best fits the given coordinate bounds, optionally with
- some additional padding on each side.
+ Returns the camera that best fits the given coordinate bounds with some
+ additional padding on each side.
@param bounds The coordinate bounds to fit to the receiver’s viewport.
@param insets The minimum padding (in screen points) that would be visible
@@ -1135,8 +1208,9 @@ MGL_EXPORT
- (MGLMapCamera *)cameraThatFitsCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(UIEdgeInsets)insets;
/**
- Returns the camera that best fits the given coordinate bounds, with the specified camera,
- optionally with some additional padding on each side.
+ Returns the camera that best fits the given coordinate bounds with some
+ additional padding on each side, matching an existing camera as much as
+ possible.
@param camera The camera that the return camera should adhere to. All values
on this camera will be manipulated except for pitch and direction.
@@ -1155,8 +1229,8 @@ MGL_EXPORT
- (MGLMapCamera *)camera:(MGLMapCamera *)camera fittingCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(UIEdgeInsets)insets;
/**
- Returns the camera that best fits the given shape, with the specified camera,
- optionally with some additional padding on each side.
+ Returns the camera that best fits the given shape with some additional padding
+ on each side, matching an existing camera as much as possible.
@param camera The camera that the return camera should adhere to. All values
on this camera will be manipulated except for pitch and direction.
@@ -1164,8 +1238,8 @@ MGL_EXPORT
@param insets The minimum padding (in screen points) that would be visible
around the returned camera object if it were set as the receiver’s camera.
@return A camera object centered on the shape's center with zoom level as high
- (close to the ground) as possible while still including the entire shape. The
- initial camera's pitch and direction will be honored.
+ (close to the ground) as possible while still including the entire shape.
+ The initial camera's pitch and direction will be honored.
@note The behavior of this method is undefined if called in response to
`UIApplicationWillTerminateNotification`; you may receive a `nil` return value
@@ -1174,16 +1248,17 @@ MGL_EXPORT
- (MGLMapCamera *)camera:(MGLMapCamera *)camera fittingShape:(MGLShape *)shape edgePadding:(UIEdgeInsets)insets;
/**
- Returns the camera that best fits the given shape, with the specified direction,
- optionally with some additional padding on each side.
+ Returns the camera that best fits the given shape with some additional padding
+ on each side while looking in the specified direction.
@param shape The shape to fit to the receiver’s viewport.
- @param direction The direction of the viewport, measured in degrees clockwise from true north.
+ @param direction The direction of the viewport, measured in degrees clockwise
+ from true north.
@param insets The minimum padding (in screen points) that would be visible
around the returned camera object if it were set as the receiver’s camera.
@return A camera object centered on the shape's center with zoom level as high
- (close to the ground) as possible while still including the entire shape. The
- camera object uses the current pitch.
+ (close to the ground) as possible while still including the entire shape.
+ The camera object uses the current pitch.
@note The behavior of this method is undefined if called in response to
`UIApplicationWillTerminateNotification`; you may receive a `nil` return value
@@ -1192,7 +1267,7 @@ MGL_EXPORT
- (MGLMapCamera *)cameraThatFitsShape:(MGLShape *)shape direction:(CLLocationDirection)direction edgePadding:(UIEdgeInsets)insets;
/**
- Returns the point in this view’s coordinate system on which to "anchor" in
+ Returns the point in this view’s coordinate system on which to “anchor” in
response to a user-initiated gesture.
For example, a pinch-to-zoom gesture would anchor the map at the midpoint of
@@ -1231,8 +1306,33 @@ MGL_EXPORT
@property (nonatomic, assign) UIEdgeInsets contentInset;
/**
+ Deprecated. Sets the distance from the edges of the map view’s frame to the edges
+ of the map view’s logical viewport with an optional transition animation.
+
+ When the value of this property is equal to `UIEdgeInsetsZero`, viewport
+ properties such as `centerCoordinate` assume a viewport that matches the map
+ view’s frame. Otherwise, those properties are inset, excluding part of the
+ frame from the viewport. For instance, if the only the top edge is inset, the
+ map center is effectively shifted downward.
+
+ When the map view’s superview is an instance of `UIViewController` whose
+ `automaticallyAdjustsScrollViewInsets` property is `YES`, the value of this
+ property may be overridden at any time.
+
+ To specify a completion handler to execute after the animation finishes, use
+ the `-setContentInset:animated:completionHandler:` method.
+
+ @param contentInset The new values to inset the content by.
+ @param animated Specify `YES` if you want the map view to animate the change to
+ the content inset or `NO` if you want the map to inset the content
+ immediately.
+ */
+- (void)setContentInset:(UIEdgeInsets)contentInset animated:(BOOL)animated __attribute__((deprecated("Use `-setContentInset:animated:completionHandler:` instead.")));
+
+/**
Sets the distance from the edges of the map view’s frame to the edges of the
- map view’s logical viewport with an optional transition animation.
+ map view’s logical viewport with an optional transition animation and
+ completion handler.
When the value of this property is equal to `UIEdgeInsetsZero`, viewport
properties such as `centerCoordinate` assume a viewport that matches the map
@@ -1248,8 +1348,9 @@ MGL_EXPORT
@param animated Specify `YES` if you want the map view to animate the change to
the content inset or `NO` if you want the map to inset the content
immediately.
+ @param completion The block executed after the animation finishes.
*/
-- (void)setContentInset:(UIEdgeInsets)contentInset animated:(BOOL)animated;
+- (void)setContentInset:(UIEdgeInsets)contentInset animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion;
#pragma mark Converting Geographic Coordinates
@@ -1332,8 +1433,6 @@ MGL_EXPORT
*/
- (CLLocationDistance)metersPerPointAtLatitude:(CLLocationDegrees)latitude;
-- (CLLocationDistance)metersPerPixelAtLatitude:(CLLocationDegrees)latitude __attribute__((unavailable("Use -metersPerPointAtLatitude:.")));
-
#pragma mark Annotating the Map
/**
@@ -1490,7 +1589,32 @@ MGL_EXPORT
@property (nonatomic, copy) NSArray<id <MGLAnnotation>> *selectedAnnotations;
/**
- Selects an annotation and displays its callout view.
+ Deprecated. Selects an annotation and displays its callout view.
+
+ The `animated` parameter determines whether the selection is animated including whether the map is
+ panned to bring the annotation into view, specifically:
+
+ | `animated` parameter | Effect |
+ |------------------|--------|
+ | `NO` | The annotation is selected, and the callout is presented. However the map is not panned to bring the annotation or callout into view. The presentation of the callout is NOT animated. |
+ | `YES` | The annotation is selected, and the callout is presented. If the annotation is not visible (or is partially visible) *and* is of type `MGLPointAnnotation`, the map is panned so that the annotation and its callout are brought into view. The annotation is *not* centered within the viewport. |
+
+ Note that a selection initiated by a single tap gesture is always animated.
+
+ To specify a completion handler to execute after the animation finishes, use
+ the `-selectAnnotation:animated:completionHandler:` method.
+
+ @param annotation The annotation object to select.
+ @param animated If `YES`, the annotation and callout view are animated on-screen.
+
+ @note In versions prior to `4.0.0` selecting an offscreen annotation did not
+ change the camera.
+ */
+- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated __attribute__((deprecated("Use `-selectAnnotation:animated:completionHandler:` instead.")));
+
+/**
+ Selects an annotation and displays its callout view with an optional completion
+ handler.
The `animated` parameter determines whether the selection is animated including whether the map is
panned to bring the annotation into view, specifically:
@@ -1504,22 +1628,25 @@ MGL_EXPORT
@param annotation The annotation object to select.
@param animated If `YES`, the annotation and callout view are animated on-screen.
+ @param completion The block executed after the animation finishes.
@note In versions prior to `4.0.0` selecting an offscreen annotation did not
change the camera.
*/
-- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated;
+- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion;
/**
:nodoc:
- Selects an annotation and displays its callout view. This method should be
- considered "alpha" and as such is liable to change.
+ Selects an annotation and displays its callout view with an optional completion
+ handler. This method should be considered "alpha" and as such is subject to
+ change.
@param annotation The annotation object to select.
- @param moveIntoView If the annotation is not visible (or is partially visible) *and* is of type `MGLPointAnnotation`, the map is panned so that the annotation and its callout are brought into view. The annotation is *not* centered within the viewport. |
+ @param moveIntoView If the annotation is not visible (or is partially visible) *and* is of type `MGLPointAnnotation`, the map is panned so that the annotation and its callout are brought into view. The annotation is *not* centered within the viewport.
@param animateSelection If `YES`, the annotation's selection state and callout view's presentation are animated.
+ @param completion The block executed after the animation finishes.
*/
-- (void)selectAnnotation:(id <MGLAnnotation>)annotation moveIntoView:(BOOL)moveIntoView animateSelection:(BOOL)animateSelection;
+- (void)selectAnnotation:(id <MGLAnnotation>)annotation moveIntoView:(BOOL)moveIntoView animateSelection:(BOOL)animateSelection completionHandler:(nullable void (^)(void))completion;
/**
Deselects an annotation and hides its callout view.
@@ -1802,12 +1929,6 @@ MGL_EXPORT
*/
@property (nonatomic) MGLMapDebugMaskOptions debugMask;
-@property (nonatomic, getter=isDebugActive) BOOL debugActive __attribute__((unavailable("Use -debugMask and -setDebugMask:.")));
-
-- (void)toggleDebug __attribute__((unavailable("Use -setDebugMask:.")));
-
-- (void)emptyMemoryCache __attribute__((unavailable));
-
@end
NS_ASSUME_NONNULL_END
diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm
index 85a2e3be92..c0b7380607 100644
--- a/platform/ios/src/MGLMapView.mm
+++ b/platform/ios/src/MGLMapView.mm
@@ -1015,28 +1015,35 @@ public:
- (void)setContentInset:(UIEdgeInsets)contentInset
{
- MGLLogDebug(@"Setting contentInset: %@", NSStringFromUIEdgeInsets(contentInset));
- [self setContentInset:contentInset animated:NO];
+ [self setContentInset:contentInset animated:NO completionHandler:nil];
}
- (void)setContentInset:(UIEdgeInsets)contentInset animated:(BOOL)animated
{
+ [self setContentInset:contentInset animated:animated completionHandler:nil];
+}
+
+- (void)setContentInset:(UIEdgeInsets)contentInset animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
+{
MGLLogDebug(@"Setting contentInset: %@ animated:", NSStringFromUIEdgeInsets(contentInset), MGLStringFromBOOL(animated));
if (UIEdgeInsetsEqualToEdgeInsets(contentInset, self.contentInset))
{
+ if (completion) {
+ completion();
+ }
return;
}
if (self.userTrackingMode == MGLUserTrackingModeNone)
{
// Don’t call -setCenterCoordinate:, which resets the user tracking mode.
- [self _setCenterCoordinate:self.centerCoordinate edgePadding:contentInset zoomLevel:self.zoomLevel direction:self.direction duration:animated ? MGLAnimationDuration : 0 animationTimingFunction:nil completionHandler:nil];
+ [self _setCenterCoordinate:self.centerCoordinate edgePadding:contentInset zoomLevel:self.zoomLevel direction:self.direction duration:animated ? MGLAnimationDuration : 0 animationTimingFunction:nil completionHandler:completion];
_contentInset = contentInset;
}
else
{
_contentInset = contentInset;
- [self didUpdateLocationWithUserTrackingAnimated:animated];
+ [self didUpdateLocationWithUserTrackingAnimated:animated completionHandler:completion];
}
// Compass, logo and attribution button constraints needs to be updated.z
@@ -1538,7 +1545,7 @@ public:
self.mbglMap.setGestureInProgress(false);
if (self.userTrackingState == MGLUserTrackingStateBegan)
{
- [self setUserTrackingMode:MGLUserTrackingModeNone animated:NO];
+ [self setUserTrackingMode:MGLUserTrackingModeNone animated:NO completionHandler:nil];
}
[self cancelTransitions];
@@ -1873,7 +1880,7 @@ public:
{
CGPoint calloutPoint = [singleTap locationInView:self];
CGRect positionRect = [self positioningRectForAnnotation:annotation defaultCalloutPoint:calloutPoint];
- [self selectAnnotation:annotation moveIntoView:YES animateSelection:YES calloutPositioningRect:positionRect];
+ [self selectAnnotation:annotation moveIntoView:YES animateSelection:YES calloutPositioningRect:positionRect completionHandler:nil];
}
else if (self.selectedAnnotation)
{
@@ -2062,6 +2069,12 @@ public:
if (twoFingerDrag.state == UIGestureRecognizerStateBegan || twoFingerDrag.state == UIGestureRecognizerStateChanged)
{
+ if (twoFingerDrag.numberOfTouches != 2)
+ {
+ twoFingerDrag.state = UIGestureRecognizerStateEnded;
+ return;
+ }
+
CGFloat gestureDistance = CGPoint([twoFingerDrag translationInView:twoFingerDrag.view]).y;
CGFloat slowdown = 2.0;
@@ -3313,40 +3326,24 @@ public:
- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds
{
- MGLLogDebug(@"Setting visibleCoordinateBounds: %@", MGLStringFromCoordinateBounds(bounds));
[self setVisibleCoordinateBounds:bounds animated:NO];
}
- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds animated:(BOOL)animated
{
- MGLLogDebug(@"Setting visibleCoordinateBounds: %@ animated: %@", MGLStringFromCoordinateBounds(bounds), MGLStringFromBOOL(animated));
- [self setVisibleCoordinateBounds:bounds edgePadding:UIEdgeInsetsZero animated:animated];
+ [self setVisibleCoordinateBounds:bounds edgePadding:UIEdgeInsetsZero animated:animated completionHandler:nil];
}
- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated
{
- MGLLogDebug(@"Setting visibleCoordinateBounds: %@ edgePadding: %@ animated: %@",
- MGLStringFromCoordinateBounds(bounds),
- NSStringFromUIEdgeInsets(insets),
- MGLStringFromBOOL(animated));
- CLLocationCoordinate2D coordinates[] = {
- {bounds.ne.latitude, bounds.sw.longitude},
- bounds.sw,
- {bounds.sw.latitude, bounds.ne.longitude},
- bounds.ne,
- };
- [self setVisibleCoordinates:coordinates
- count:sizeof(coordinates) / sizeof(coordinates[0])
- edgePadding:insets
- animated:animated];
+ [self setVisibleCoordinateBounds:bounds edgePadding:insets animated:animated completionHandler:nil];
}
-- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(UIEdgeInsets)insets direction:(CLLocationDirection)direction animated:(BOOL)animated
+- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
{
- MGLLogDebug(@"Setting visibleCoordinateBounds: %@ edgePadding: %@ direction: %f animated: %@",
+ MGLLogDebug(@"Setting visibleCoordinateBounds: %@ edgePadding: %@ animated: %@",
MGLStringFromCoordinateBounds(bounds),
NSStringFromUIEdgeInsets(insets),
- direction,
MGLStringFromBOOL(animated));
CLLocationCoordinate2D coordinates[] = {
{bounds.ne.latitude, bounds.sw.longitude},
@@ -3357,8 +3354,10 @@ public:
[self setVisibleCoordinates:coordinates
count:sizeof(coordinates) / sizeof(coordinates[0])
edgePadding:insets
- direction:direction
- animated:animated];
+ direction:self.direction
+ duration:animated ? MGLAnimationDuration : 0
+ animationTimingFunction:nil
+ completionHandler:completion];
}
- (void)setVisibleCoordinates:(const CLLocationCoordinate2D *)coordinates count:(NSUInteger)count edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated
@@ -3367,17 +3366,7 @@ public:
count,
NSStringFromUIEdgeInsets(insets),
MGLStringFromBOOL(animated));
- [self setVisibleCoordinates:coordinates count:count edgePadding:insets direction:self.direction animated:animated];
-}
-
-- (void)setVisibleCoordinates:(const CLLocationCoordinate2D *)coordinates count:(NSUInteger)count edgePadding:(UIEdgeInsets)insets direction:(CLLocationDirection)direction animated:(BOOL)animated
-{
- MGLLogDebug(@"Setting: %lu coordinates edgePadding: %@ direction: %f animated: %@",
- count,
- NSStringFromUIEdgeInsets(insets),
- direction,
- MGLStringFromBOOL(animated));
- [self setVisibleCoordinates:coordinates count:count edgePadding:insets direction:direction duration:animated ? MGLAnimationDuration : 0 animationTimingFunction:nil];
+ [self setVisibleCoordinates:coordinates count:count edgePadding:insets direction:self.direction duration:animated ? MGLAnimationDuration : 0 animationTimingFunction:nil];
}
- (void)setVisibleCoordinates:(const CLLocationCoordinate2D *)coordinates count:(NSUInteger)count edgePadding:(UIEdgeInsets)insets direction:(CLLocationDirection)direction duration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function {
@@ -4679,7 +4668,6 @@ public:
- (void)setSelectedAnnotations:(NSArray<id <MGLAnnotation>> *)selectedAnnotations
{
- MGLLogDebug(@"Selecting: %lu annotations", selectedAnnotations.count);
if ( ! selectedAnnotations.count) return;
id <MGLAnnotation> firstAnnotation = selectedAnnotations[0];
@@ -4688,22 +4676,27 @@ public:
if ([firstAnnotation isKindOfClass:[MGLMultiPoint class]]) return;
- [self selectAnnotation:firstAnnotation animated:YES];
+ [self selectAnnotation:firstAnnotation animated:YES completionHandler:nil];
}
- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated
{
- [self selectAnnotation:annotation moveIntoView:animated animateSelection:animated];
+ [self selectAnnotation:annotation animated:animated completionHandler:nil];
}
-- (void)selectAnnotation:(id <MGLAnnotation>)annotation moveIntoView:(BOOL)moveIntoView animateSelection:(BOOL)animateSelection
+- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
+{
+ [self selectAnnotation:annotation moveIntoView:animated animateSelection:animated completionHandler:completion];
+}
+
+- (void)selectAnnotation:(id <MGLAnnotation>)annotation moveIntoView:(BOOL)moveIntoView animateSelection:(BOOL)animateSelection completionHandler:(nullable void (^)(void))completion
{
MGLLogDebug(@"Selecting annotation: %@ moveIntoView: %@ animateSelection: %@", annotation, MGLStringFromBOOL(moveIntoView), MGLStringFromBOOL(animateSelection));
CGRect positioningRect = [self positioningRectForAnnotation:annotation defaultCalloutPoint:CGPointZero];
- [self selectAnnotation:annotation moveIntoView:moveIntoView animateSelection:animateSelection calloutPositioningRect:positioningRect];
+ [self selectAnnotation:annotation moveIntoView:moveIntoView animateSelection:animateSelection calloutPositioningRect:positioningRect completionHandler:completion];
}
-- (void)selectAnnotation:(id <MGLAnnotation>)annotation moveIntoView:(BOOL)moveIntoView animateSelection:(BOOL)animateSelection calloutPositioningRect:(CGRect)calloutPositioningRect
+- (void)selectAnnotation:(id <MGLAnnotation>)annotation moveIntoView:(BOOL)moveIntoView animateSelection:(BOOL)animateSelection calloutPositioningRect:(CGRect)calloutPositioningRect completionHandler:(nullable void (^)(void))completion
{
if ( ! annotation) return;
@@ -4913,7 +4906,11 @@ public:
{
CGPoint center = CGPointMake(CGRectGetMidX(constrainedRect), CGRectGetMidY(constrainedRect));
CLLocationCoordinate2D centerCoord = [self convertPoint:center toCoordinateFromView:self];
- [self setCenterCoordinate:centerCoord animated:animateSelection];
+ [self setCenterCoordinate:centerCoord zoomLevel:self.zoomLevel direction:self.direction animated:animateSelection completionHandler:completion];
+ }
+ else if (completion)
+ {
+ completion();
}
}
@@ -5099,20 +5096,30 @@ public:
- (void)showAnnotations:(NSArray<id <MGLAnnotation>> *)annotations animated:(BOOL)animated
{
- MGLLogDebug(@"Showing: %lu annotations animated: %@", annotations.count, MGLStringFromBOOL(animated));
CGFloat maximumPadding = 100;
CGFloat yPadding = (self.frame.size.height / 5 <= maximumPadding) ? (self.frame.size.height / 5) : maximumPadding;
CGFloat xPadding = (self.frame.size.width / 5 <= maximumPadding) ? (self.frame.size.width / 5) : maximumPadding;
UIEdgeInsets edgeInsets = UIEdgeInsetsMake(yPadding, xPadding, yPadding, xPadding);
- [self showAnnotations:annotations edgePadding:edgeInsets animated:animated];
+ [self showAnnotations:annotations edgePadding:edgeInsets animated:animated completionHandler:nil];
}
- (void)showAnnotations:(NSArray<id <MGLAnnotation>> *)annotations edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated
{
+ [self showAnnotations:annotations edgePadding:insets animated:animated completionHandler:nil];
+}
+
+- (void)showAnnotations:(NSArray<id <MGLAnnotation>> *)annotations edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
+{
MGLLogDebug(@"Showing: %lu annotations edgePadding: %@ animated: %@", annotations.count, NSStringFromUIEdgeInsets(insets), MGLStringFromBOOL(animated));
- if ( ! annotations || ! annotations.count) return;
+ if ( ! annotations.count)
+ {
+ if (completion) {
+ completion();
+ }
+ return;
+ }
mbgl::LatLngBounds bounds = mbgl::LatLngBounds::empty();
@@ -5130,7 +5137,8 @@ public:
[self setVisibleCoordinateBounds:MGLCoordinateBoundsFromLatLngBounds(bounds)
edgePadding:insets
- animated:animated];
+ animated:animated
+ completionHandler:completion];
}
@@ -5293,7 +5301,7 @@ public:
[self.delegate mapViewDidStopLocatingUser:self];
}
- [self setUserTrackingMode:MGLUserTrackingModeNone animated:YES];
+ [self setUserTrackingMode:MGLUserTrackingModeNone animated:YES completionHandler:nil];
[self.userLocationAnnotationView removeFromSuperview];
self.userLocationAnnotationView = nil;
@@ -5333,14 +5341,25 @@ public:
- (void)setUserTrackingMode:(MGLUserTrackingMode)mode
{
- MGLLogDebug(@"Setting userTrackingMode: %lu", mode);
- [self setUserTrackingMode:mode animated:YES];
+ [self setUserTrackingMode:mode animated:YES completionHandler:nil];
}
- (void)setUserTrackingMode:(MGLUserTrackingMode)mode animated:(BOOL)animated
{
+ [self setUserTrackingMode:mode animated:animated completionHandler:nil];
+}
+
+- (void)setUserTrackingMode:(MGLUserTrackingMode)mode animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
+{
MGLLogDebug(@"Setting userTrackingMode: %lu animated: %@", mode, MGLStringFromBOOL(animated));
- if (mode == _userTrackingMode) return;
+ if (mode == _userTrackingMode)
+ {
+ if (completion)
+ {
+ completion();
+ }
+ return;
+ }
MGLUserTrackingMode oldMode = _userTrackingMode;
[self willChangeValueForKey:@"userTrackingMode"];
@@ -5385,13 +5404,14 @@ public:
}
}
- if (_userTrackingMode != MGLUserTrackingModeNone)
+ CLLocation *location;
+ if (_userTrackingMode != MGLUserTrackingModeNone && (location = self.userLocation.location) && self.userLocationAnnotationView)
{
- CLLocation *location = self.userLocation.location;
- if (location && self.userLocationAnnotationView)
- {
- [self locationManager:self.locationManager didUpdateLocations:@[location] animated:animated];
- }
+ [self locationManager:self.locationManager didUpdateLocations:@[location] animated:animated completionHandler:completion];
+ }
+ else if (completion)
+ {
+ completion();
}
[self validateUserHeadingUpdating];
@@ -5415,20 +5435,25 @@ public:
CLLocation *location = self.userLocation.location;
if (location)
{
- [self locationManager:self.locationManager didUpdateLocations:@[location] animated:animated];
+ [self locationManager:self.locationManager didUpdateLocations:@[location] animated:animated completionHandler:nil];
}
}
}
- (void)setTargetCoordinate:(CLLocationCoordinate2D)targetCoordinate
{
- MGLLogDebug(@"Setting targetCoordinate: %@", MGLStringFromCLLocationCoordinate2D(targetCoordinate));
- [self setTargetCoordinate:targetCoordinate animated:YES];
+ [self setTargetCoordinate:targetCoordinate animated:YES completionHandler:nil];
}
- (void)setTargetCoordinate:(CLLocationCoordinate2D)targetCoordinate animated:(BOOL)animated
{
+ [self setTargetCoordinate:targetCoordinate animated:animated completionHandler:nil];
+}
+
+- (void)setTargetCoordinate:(CLLocationCoordinate2D)targetCoordinate animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
+{
MGLLogDebug(@"Setting targetCoordinate: %@ animated: %@", MGLStringFromCLLocationCoordinate2D(targetCoordinate), MGLStringFromBOOL(animated));
+ BOOL isSynchronous = YES;
if (targetCoordinate.latitude != self.targetCoordinate.latitude
|| targetCoordinate.longitude != self.targetCoordinate.longitude)
{
@@ -5437,13 +5462,17 @@ public:
{
self.userTrackingState = MGLUserTrackingStatePossible;
- CLLocation *location = self.userLocation.location;
- if (location)
+ if (CLLocation *location = self.userLocation.location)
{
- [self locationManager:self.locationManager didUpdateLocations:@[location] animated:animated];
+ isSynchronous = NO;
+ [self locationManager:self.locationManager didUpdateLocations:@[location] animated:animated completionHandler:completion];
}
}
}
+ if (isSynchronous && completion)
+ {
+ completion();
+ }
}
- (void)setShowsUserHeadingIndicator:(BOOL)showsUserHeadingIndicator
@@ -5475,10 +5504,10 @@ public:
- (void)locationManager:(id<MGLLocationManager>)manager didUpdateLocations:(NSArray *)locations
{
- [self locationManager:manager didUpdateLocations:locations animated:YES];
+ [self locationManager:manager didUpdateLocations:locations animated:YES completionHandler:nil];
}
-- (void)locationManager:(__unused id<MGLLocationManager>)manager didUpdateLocations:(NSArray *)locations animated:(BOOL)animated
+- (void)locationManager:(__unused id<MGLLocationManager>)manager didUpdateLocations:(NSArray *)locations animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
{
CLLocation *oldLocation = self.userLocation.location;
CLLocation *newLocation = locations.lastObject;
@@ -5500,7 +5529,7 @@ public:
}
}
- [self didUpdateLocationWithUserTrackingAnimated:animated];
+ [self didUpdateLocationWithUserTrackingAnimated:animated completionHandler:completion];
NSTimeInterval duration = MGLAnimationDuration;
if (oldLocation && ! CGPointEqualToPoint(self.userLocationAnnotationView.center, CGPointZero))
@@ -5517,13 +5546,17 @@ public:
}
}
-- (void)didUpdateLocationWithUserTrackingAnimated:(BOOL)animated
+- (void)didUpdateLocationWithUserTrackingAnimated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
{
CLLocation *location = self.userLocation.location;
if ( ! _showsUserLocation || ! location
|| ! CLLocationCoordinate2DIsValid(location.coordinate)
|| self.userTrackingMode == MGLUserTrackingModeNone)
{
+ if (completion)
+ {
+ completion();
+ }
return;
}
@@ -5534,6 +5567,10 @@ public:
if (std::abs(currentPoint.x - correctPoint.x) <= 1.0 && std::abs(currentPoint.y - correctPoint.y) <= 1.0
&& self.userTrackingMode != MGLUserTrackingModeFollowWithCourse)
{
+ if (completion)
+ {
+ completion();
+ }
return;
}
@@ -5543,25 +5580,25 @@ public:
if (self.userTrackingState != MGLUserTrackingStateBegan)
{
// Keep both the user and the destination in view.
- [self didUpdateLocationWithTargetAnimated:animated];
+ [self didUpdateLocationWithTargetAnimated:animated completionHandler:completion];
}
}
else if (self.userTrackingState == MGLUserTrackingStatePossible)
{
// The first location update is often a great distance away from the
// current viewport, so fly there to provide additional context.
- [self didUpdateLocationSignificantlyAnimated:animated];
+ [self didUpdateLocationSignificantlyAnimated:animated completionHandler:completion];
}
else if (self.userTrackingState == MGLUserTrackingStateChanged)
{
// Subsequent updates get a more subtle animation.
- [self didUpdateLocationIncrementallyAnimated:animated];
+ [self didUpdateLocationIncrementallyAnimated:animated completionHandler:completion];
}
[self unrotateIfNeededAnimated:YES];
}
/// Changes the viewport based on an incremental location update.
-- (void)didUpdateLocationIncrementallyAnimated:(BOOL)animated
+- (void)didUpdateLocationIncrementallyAnimated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
{
[self _setCenterCoordinate:self.userLocation.location.coordinate
edgePadding:self.edgePaddingForFollowing
@@ -5569,12 +5606,12 @@ public:
direction:self.directionByFollowingWithCourse
duration:animated ? MGLUserLocationAnimationDuration : 0
animationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]
- completionHandler:nil];
+ completionHandler:completion];
}
/// Changes the viewport based on a significant location update, such as the
/// first location update.
-- (void)didUpdateLocationSignificantlyAnimated:(BOOL)animated
+- (void)didUpdateLocationSignificantlyAnimated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
{
if (_distanceFromOldUserLocation >= MGLDistanceThresholdForCameraPause) {
@@ -5606,25 +5643,33 @@ public:
{
strongSelf.userTrackingState = MGLUserTrackingStateChanged;
}
+ if (completion)
+ {
+ completion();
+ }
}];
}
/// Changes the viewport based on a location update in the presence of a target
/// coordinate that must also be displayed on the map concurrently.
-- (void)didUpdateLocationWithTargetAnimated:(BOOL)animated
+- (void)didUpdateLocationWithTargetAnimated:(BOOL)animated completionHandler:(nullable void (^)(void))completion
{
BOOL firstUpdate = self.userTrackingState == MGLUserTrackingStatePossible;
- void (^completion)(void);
+ void (^animationCompletion)(void);
if (animated && firstUpdate)
{
self.userTrackingState = MGLUserTrackingStateBegan;
__weak MGLMapView *weakSelf = self;
- completion = ^{
+ animationCompletion = ^{
MGLMapView *strongSelf = weakSelf;
if (strongSelf.userTrackingState == MGLUserTrackingStateBegan)
{
strongSelf.userTrackingState = MGLUserTrackingStateChanged;
}
+ if (completion)
+ {
+ completion();
+ }
};
}
@@ -5648,7 +5693,7 @@ public:
direction:self.directionByFollowingWithCourse
duration:animated ? MGLUserLocationAnimationDuration : 0
animationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]
- completionHandler:completion];
+ completionHandler:animationCompletion];
}
/// Returns the edge padding to apply when moving the map to a tracked location.
@@ -6189,6 +6234,14 @@ public:
}
}
+- (BOOL)shouldRemoveStyleImage:(NSString *)imageName {
+ if ([self.delegate respondsToSelector:@selector(mapView:shouldRemoveStyleImage:)]) {
+ return [self.delegate mapView:self shouldRemoveStyleImage:imageName];
+ }
+
+ return YES;
+}
+
- (void)updateUserLocationAnnotationView
{
[self updateUserLocationAnnotationViewAnimatedWithDuration:0];
diff --git a/platform/ios/src/MGLMapViewDelegate.h b/platform/ios/src/MGLMapViewDelegate.h
index f5249d1797..3ddb7b007f 100644
--- a/platform/ios/src/MGLMapViewDelegate.h
+++ b/platform/ios/src/MGLMapViewDelegate.h
@@ -277,6 +277,20 @@ NS_ASSUME_NONNULL_BEGIN
- (nullable UIImage *)mapView:(MGLMapView *)mapView didFailToLoadImage:(NSString *)imageName;
+/**
+ Asks the delegate whether the map view should evict cached images.
+
+ This method is called in two scenarios: when the cumulative size of unused images
+ exceeds the cache size or when the last tile that includes the image is removed from
+ memory.
+
+ @param mapView The map view that is evicting the image.
+ @param imageName The image name that is going to be removed.
+ @return A Boolean value indicating whether the map view should evict
+ the cached image.
+ */
+- (BOOL)mapView:(MGLMapView *)mapView shouldRemoveStyleImage:(NSString *)imageName;
+
#pragma mark Tracking User Location
/**
diff --git a/platform/ios/src/MGLMapView_Private.h b/platform/ios/src/MGLMapView_Private.h
index 08c1ff410d..5aa4902a91 100644
--- a/platform/ios/src/MGLMapView_Private.h
+++ b/platform/ios/src/MGLMapView_Private.h
@@ -41,6 +41,7 @@ FOUNDATION_EXTERN MGL_EXPORT MGLExceptionName const _Nonnull MGLUnderlyingMapUna
- (void)mapViewDidFinishLoadingStyle;
- (void)sourceDidChange:(nonnull MGLSource *)source;
- (void)didFailToLoadImage:(nonnull NSString *)imageName;
+- (BOOL)shouldRemoveStyleImage:(nonnull NSString *)imageName;
/** Triggers another render pass even when it is not necessary. */
- (void)setNeedsRerender;
diff --git a/platform/ios/test/MGLAnnotationViewTests.m b/platform/ios/test/MGLAnnotationViewTests.m
index 6c3cd6ea4a..bb85ea8c8a 100644
--- a/platform/ios/test/MGLAnnotationViewTests.m
+++ b/platform/ios/test/MGLAnnotationViewTests.m
@@ -134,7 +134,7 @@ static NSString * const MGLTestAnnotationReuseIdentifer = @"MGLTestAnnotationReu
XCTAssertNil(_mapView.selectedAnnotations.firstObject, @"There should be no selected annotation");
// First selection
- [_mapView selectAnnotation:annotation animated:NO];
+ [_mapView selectAnnotation:annotation animated:NO completionHandler:nil];
selectionCount++;
XCTAssert(_mapView.selectedAnnotations.count == 1, @"There should only be 1 selected annotation");
@@ -190,7 +190,7 @@ static NSString * const MGLTestAnnotationReuseIdentifer = @"MGLTestAnnotationReu
XCTAssert(MGLCoordinateInCoordinateBounds(point.coordinate, coordinateBounds), @"The test point should be within the visible map view");
// Select on screen annotation (DO NOT ADD FIRST).
- [self.mapView selectAnnotation:point animated:YES];
+ [self.mapView selectAnnotation:point animated:YES completionHandler:nil];
// Expect - the camera NOT to move.
MGLCameraChangeReason reasonAfter = self.mapView.cameraChangeReasonBitmask;
@@ -235,7 +235,7 @@ static NSString * const MGLTestAnnotationReuseIdentifer = @"MGLTestAnnotationReu
XCTAssert(self.mapView.selectedAnnotations.count == 0, @"There should be 0 selected annotations");
- [self.mapView selectAnnotation:point animated:NO];
+ [self.mapView selectAnnotation:point animated:NO completionHandler:nil];
XCTAssert(self.mapView.selectedAnnotations.count == 0, @"There should be 0 selected annotations");
}
diff --git a/platform/ios/test/MGLMapAccessibilityElementTests.m b/platform/ios/test/MGLMapAccessibilityElementTests.m
index 916461e708..2312d1d406 100644
--- a/platform/ios/test/MGLMapAccessibilityElementTests.m
+++ b/platform/ios/test/MGLMapAccessibilityElementTests.m
@@ -8,6 +8,11 @@
@implementation MGLMapAccessibilityElementTests
+- (void)setUp {
+ // FIXME: https://github.com/mapbox/mapbox-gl-native/issues/14908
+ XCTAssertEqualObjects(NSLocale.currentLocale.localeIdentifier, @"en_US", @"Device locale must be en_US for these tests to pass.");
+}
+
- (void)testFeatureLabels {
MGLPointFeature *feature = [[MGLPointFeature alloc] init];
feature.attributes = @{
diff --git a/platform/ios/test/MGLMapViewDelegateIntegrationTests.swift b/platform/ios/test/MGLMapViewDelegateIntegrationTests.swift
index 1330281faa..172538c65b 100644
--- a/platform/ios/test/MGLMapViewDelegateIntegrationTests.swift
+++ b/platform/ios/test/MGLMapViewDelegateIntegrationTests.swift
@@ -98,4 +98,6 @@ extension MGLMapViewDelegateIntegrationTests: MGLMapViewDelegate {
func mapViewUserLocationAnchorPoint(_ mapView: MGLMapView) -> CGPoint { return CGPoint(x: 100, y: 100) }
func mapView(_ mapView: MGLMapView, didFailToLoadImage imageName: String) -> UIImage? { return nil }
+
+ func mapView(_ mapView: MGLMapView, shouldRemoveStyleImage imageName: String) -> Bool { return false }
}
diff --git a/platform/linux/config.cmake b/platform/linux/config.cmake
index 7cc1f1fe4d..aa65ddb606 100644
--- a/platform/linux/config.cmake
+++ b/platform/linux/config.cmake
@@ -56,6 +56,7 @@ macro(mbgl_platform_core)
PRIVATE platform/default/src/mbgl/text/unaccent.cpp
PRIVATE platform/default/include/mbgl/text/unaccent.hpp
PRIVATE platform/default/src/mbgl/util/utf.cpp
+ PRIVATE platform/default/src/mbgl/util/format_number.cpp
# Image handling
PRIVATE platform/default/src/mbgl/util/image.cpp
@@ -83,10 +84,21 @@ macro(mbgl_platform_core)
target_add_mason_package(mbgl-core PUBLIC libpng)
target_add_mason_package(mbgl-core PUBLIC libjpeg-turbo)
+ target_add_mason_package(mbgl-core PRIVATE icu)
+
+ # Ignore warning caused by ICU header unistr.h in some CI environments
+ set_source_files_properties(platform/default/src/mbgl/util/format_number.cpp PROPERTIES COMPILE_FLAGS -Wno-error=shadow)
+
+ # Link all ICU libraries (by default only libicuuc is linked)
+ find_library(LIBICUI18N NAMES icui18n HINTS ${MASON_PACKAGE_icu_INCLUDE_DIRS}/../lib)
+ find_library(LIBICUUC NAMES icuuc HINTS ${MASON_PACKAGE_icu_INCLUDE_DIRS}/../lib)
+ find_library(LIBICUDATA NAMES icudata HINTS ${MASON_PACKAGE_icu_INCLUDE_DIRS}/../lib)
target_link_libraries(mbgl-core
+ PRIVATE ${LIBICUI18N}
+ PRIVATE ${LIBICUUC}
+ PRIVATE ${LIBICUDATA}
PRIVATE nunicode
- PRIVATE icu
PUBLIC -lz
)
diff --git a/platform/macos/CHANGELOG.md b/platform/macos/CHANGELOG.md
index 650814e56b..84e9be89a7 100644
--- a/platform/macos/CHANGELOG.md
+++ b/platform/macos/CHANGELOG.md
@@ -9,11 +9,19 @@
### Styles and rendering
* Setting `MGLMapView.contentInset` now moves the map’s focal point to the center of the content frame after insetting. ([#14664](https://github.com/mapbox/mapbox-gl-native/pull/14664))
+* Added the `-[MGLMapViewDelegate mapView:shouldRemoveStyleImage:]` method for optimizing style image caching. ([#14769](https://github.com/mapbox/mapbox-gl-native/pull/14769))
### Other changes
* The `-[MGLMapView setCamera:withDuration:animationTimingFunction:edgePadding:completionHandler:]` method now adds the current value of the `MGLMapView.contentInsets` property to the `edgePadding` parameter. ([#14813](https://github.com/mapbox/mapbox-gl-native/pull/14813))
+### Other changes
+
+* Added variants of multiple animated `MGLMapView` methods that accept completion handlers ([#14381](https://github.com/mapbox/mapbox-gl-native/pull/14381)):
+ * `-[MGLMapView setVisibleCoordinateBounds:edgePadding:animated:completionHandler:]`
+ * `-[MGLMapView setContentInsets:animated:completionHandler:]`
+ * `-[MGLMapView showAnnotations:edgePadding:animated:completionHandler:]`
+
## 0.14.0 - May 22, 2018
### Styles and rendering
diff --git a/platform/macos/DEVELOPING.md b/platform/macos/DEVELOPING.md
index 8792a4b974..b9c914a976 100644
--- a/platform/macos/DEVELOPING.md
+++ b/platform/macos/DEVELOPING.md
@@ -114,6 +114,10 @@ make darwin-update-examples
[SourceKitten](https://github.com/jpsim/SourceKitten/) is required and will be installed automatically using Homebrew.
+### Customizing compilation settings
+
+You can provide an optional and custom [`xcconfig`](https://help.apple.com/xcode/mac/current/#/dev745c5c974) file named `platform/darwin/developer.xcconfig` to set custom build options. This file is ignored by git. These custom settings apply to all configurations (`Debug`, `Release`), but do **not** apply to the core `mbgl` files. This mechanism allows you to try different compiler settings (for example when testing an Xcode beta).
+
## Testing
`make macos-test` builds and runs unit tests of cross-platform code as well as the SDK.
diff --git a/platform/macos/macos.xcodeproj/project.pbxproj b/platform/macos/macos.xcodeproj/project.pbxproj
index 93a0b616a7..79540e1fef 100644
--- a/platform/macos/macos.xcodeproj/project.pbxproj
+++ b/platform/macos/macos.xcodeproj/project.pbxproj
@@ -1966,6 +1966,7 @@
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "-";
@@ -2032,6 +2033,7 @@
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "-";
diff --git a/platform/macos/src/MGLMapView+Impl.h b/platform/macos/src/MGLMapView+Impl.h
index 19583c17e5..2d523716d4 100644
--- a/platform/macos/src/MGLMapView+Impl.h
+++ b/platform/macos/src/MGLMapView+Impl.h
@@ -36,6 +36,7 @@ public:
void onDidFinishLoadingStyle() override;
void onSourceChanged(mbgl::style::Source& source) override;
void onDidBecomeIdle() override;
+ bool onCanRemoveUnusedStyleImage(const std::string& imageIdentifier) override;
protected:
/// Cocoa map view that this adapter bridges to.
diff --git a/platform/macos/src/MGLMapView+Impl.mm b/platform/macos/src/MGLMapView+Impl.mm
index 7be5545671..2354f67a6d 100644
--- a/platform/macos/src/MGLMapView+Impl.mm
+++ b/platform/macos/src/MGLMapView+Impl.mm
@@ -94,3 +94,8 @@ void MGLMapViewImpl::onSourceChanged(mbgl::style::Source& source) {
MGLSource * nativeSource = [mapView.style sourceWithIdentifier:identifier];
[mapView sourceDidChange:nativeSource];
}
+
+bool MGLMapViewImpl::onCanRemoveUnusedStyleImage(const std::string &imageIdentifier) {
+ NSString *imageName = [NSString stringWithUTF8String:imageIdentifier.c_str()];
+ return [mapView shouldRemoveStyleImage:imageName];
+}
diff --git a/platform/macos/src/MGLMapView.h b/platform/macos/src/MGLMapView.h
index 35db5257d9..f5fcee5b25 100644
--- a/platform/macos/src/MGLMapView.h
+++ b/platform/macos/src/MGLMapView.h
@@ -434,8 +434,16 @@ MGL_EXPORT IB_DESIGNABLE
- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds animated:(BOOL)animated;
/**
- Changes the receiver’s viewport to fit the given coordinate bounds and
- optionally some additional padding on each side.
+ Changes the receiver’s viewport to fit the given coordinate bounds with some
+ additional padding on each side.
+
+ To bring both sides of the antimeridian or international date line into view,
+ specify some longitudes less than −180 degrees or greater than 180 degrees. For
+ example, to show both Tokyo and San Francisco simultaneously, you could set the
+ visible bounds to extend from (35.68476, −220.24257) to (37.78428, −122.41310).
+
+ To specify a completion handler to execute after the animation finishes, use
+ the `-setVisibleCoordinateBounds:edgePadding:animated:completionHandler:` method.
@param bounds The bounds that the viewport will show in its entirety.
@param insets The minimum padding (in screen points) that will be visible
@@ -446,6 +454,24 @@ MGL_EXPORT IB_DESIGNABLE
- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(NSEdgeInsets)insets animated:(BOOL)animated;
/**
+ Changes the receiver’s viewport to fit the given coordinate bounds with some
+ additional padding on each side, optionally calling a completion handler.
+
+ To bring both sides of the antimeridian or international date line into view,
+ specify some longitudes less than −180 degrees or greater than 180 degrees. For
+ example, to show both Tokyo and San Francisco simultaneously, you could set the
+ visible bounds to extend from (35.68476, −220.24257) to (37.78428, −122.41310).
+
+ @param bounds The bounds that the viewport will show in its entirety.
+ @param insets The minimum padding (in screen points) that will be visible
+ around the given coordinate bounds.
+ @param animated Specify `YES` to animate the change by smoothly scrolling and
+ zooming or `NO` to immediately display the given bounds.
+ @param completion The block executed after the animation finishes.
+ */
+- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(NSEdgeInsets)insets animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion;
+
+/**
Sets the visible region so that the map displays the specified annotations.
Calling this method updates the value in the `visibleCoordinateBounds` property
@@ -465,6 +491,9 @@ MGL_EXPORT IB_DESIGNABLE
Calling this method updates the value in the `visibleCoordinateBounds` property
and potentially other properties to reflect the new map region.
+
+ To specify a completion handler to execute after the animation finishes, use
+ the `-showAnnotations:edgePadding:animated:completionHandler:` method.
@param annotations The annotations that you want to be visible in the map.
@param insets The minimum padding (in screen points) around the edges of the
@@ -475,6 +504,23 @@ MGL_EXPORT IB_DESIGNABLE
- (void)showAnnotations:(NSArray<id <MGLAnnotation>> *)annotations edgePadding:(NSEdgeInsets)insets animated:(BOOL)animated;
/**
+ Sets the visible region so that the map displays the specified annotations with
+ the specified amount of padding on each side and an optional completion
+ handler.
+
+ Calling this method updates the value in the `visibleCoordinateBounds` property
+ and potentially other properties to reflect the new map region.
+
+ @param annotations The annotations that you want to be visible in the map.
+ @param insets The minimum padding (in screen points) around the edges of the
+ map view to keep clear of annotations.
+ @param animated `YES` if you want the map region change to be animated, or `NO`
+ if you want the map to display the new region immediately without animations.
+ @param completion The block executed after the animation finishes.
+ */
+- (void)showAnnotations:(NSArray<id <MGLAnnotation>> *)annotations edgePadding:(NSEdgeInsets)insets animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion;
+
+/**
Returns the camera that best fits the given coordinate bounds.
@param bounds The coordinate bounds to fit to the receiver’s viewport.
@@ -486,8 +532,8 @@ MGL_EXPORT IB_DESIGNABLE
- (MGLMapCamera *)cameraThatFitsCoordinateBounds:(MGLCoordinateBounds)bounds;
/**
- Returns the camera that best fits the given coordinate bounds, optionally with
- some additional padding on each side.
+ Returns the camera that best fits the given coordinate bounds with some
+ additional padding on each side.
@param bounds The coordinate bounds to fit to the receiver’s viewport.
@param insets The minimum padding (in screen points) that would be visible
@@ -500,8 +546,9 @@ MGL_EXPORT IB_DESIGNABLE
- (MGLMapCamera *)cameraThatFitsCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(NSEdgeInsets)insets;
/**
- Returns the camera that best fits the given coordinate bounds, with the specified camera,
- optionally with some additional padding on each side.
+ Returns the camera that best fits the given coordinate bounds with some
+ additional padding on each side, matching an existing camera as much as
+ possible.
@param camera The camera that the return camera should adhere to. All values
on this camera will be manipulated except for pitch and direction.
@@ -516,8 +563,8 @@ MGL_EXPORT IB_DESIGNABLE
- (MGLMapCamera *)camera:(MGLMapCamera *)camera fittingCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(NSEdgeInsets)insets;
/**
- Returns the camera that best fits the given shape, with the specified camera,
- optionally with some additional padding on each side.
+ Returns the camera that best fits the given shape with some additional padding
+ on each side, matching an existing camera as much as possible.
@param camera The camera that the return camera should adhere to. All values
on this camera will be manipulated except for pitch and direction.
@@ -525,22 +572,23 @@ MGL_EXPORT IB_DESIGNABLE
@param insets The minimum padding (in screen points) that would be visible
around the returned camera object if it were set as the receiver’s camera.
@return A camera object centered on the shape's center with zoom level as high
- (close to the ground) as possible while still including the entire shape. The
- initial camera's pitch and direction will be honored.
+ (close to the ground) as possible while still including the entire shape.
+ The initial camera's pitch and direction will be honored.
*/
- (MGLMapCamera *)camera:(MGLMapCamera *)camera fittingShape:(MGLShape *)shape edgePadding:(NSEdgeInsets)insets;
/**
- Returns the camera that best fits the given shape, with the specified direction,
- optionally with some additional padding on each side.
+ Returns the camera that best fits the given shape with some additional padding
+ on each side while looking in the specified direction.
@param shape The shape to fit to the receiver’s viewport.
- @param direction The direction of the viewport, measured in degrees clockwise from true north.
+ @param direction The direction of the viewport, measured in degrees clockwise
+ from true north.
@param insets The minimum padding (in screen points) that would be visible
around the returned camera object if it were set as the receiver’s camera.
- @return A camera object centered on the shape's center with zoom level as high
- (close to the ground) as possible while still including the entire shape. The
- camera object uses the current pitch.
+ @return A camera object centered on the shape's center with zoom level as high
+ (close to the ground) as possible while still including the entire shape.
+ The camera object uses the current pitch.
*/
- (MGLMapCamera *)cameraThatFitsShape:(MGLShape *)shape direction:(CLLocationDirection)direction edgePadding:(NSEdgeInsets)insets;
@@ -578,8 +626,8 @@ MGL_EXPORT IB_DESIGNABLE
@property (nonatomic, assign) NSEdgeInsets contentInsets;
/**
- Sets the distance from the edges of the map view’s frame to the edges of the
- map view’s logical viewport, with an optional transition animation.
+ Deprecated. Sets the distance from the edges of the map view’s frame to the
+ edges of the map view’s logical viewport, with an optional transition animation.
When the value of this property is equal to `NSEdgeInsetsZero`, viewport
properties such as `centerCoordinate` assume a viewport that matches the map
@@ -589,13 +637,38 @@ MGL_EXPORT IB_DESIGNABLE
When the value of the `automaticallyAdjustsContentInsets` property is `YES`,
the value of this property may be overridden at any time.
+
+ To specify a completion handler to execute after the animation finishes, use
+ the `-setContentInsets:animated:completionHandler:` method.
@param contentInsets The new values to inset the content by.
@param animated Specify `YES` if you want the map view to animate the change to
the content insets or `NO` if you want the map to inset the content
immediately.
*/
-- (void)setContentInsets:(NSEdgeInsets)contentInsets animated:(BOOL)animated;
+- (void)setContentInsets:(NSEdgeInsets)contentInsets animated:(BOOL)animated __attribute__((deprecated("Use `-setContentInsets:animated:completionHandler:` instead.")));
+
+/**
+ Sets the distance from the edges of the map view’s frame to the edges of the
+ map view’s logical viewport with an optional transition animation and
+ completion handler.
+
+ When the value of this property is equal to `NSEdgeInsetsZero`, viewport
+ properties such as `centerCoordinate` assume a viewport that matches the map
+ view’s frame. Otherwise, those properties are inset, excluding part of the
+ frame from the viewport. For instance, if the only the top edge is inset, the
+ map center is effectively shifted downward.
+
+ When the value of the `automaticallyAdjustsContentInsets` property is `YES`,
+ the value of this property may be overridden at any time.
+
+ @param contentInsets The new values to inset the content by.
+ @param animated Specify `YES` if you want the map view to animate the change to
+ the content insets or `NO` if you want the map to inset the content
+ immediately.
+ @param completion The block executed after the animation finishes.
+ */
+- (void)setContentInsets:(NSEdgeInsets)contentInsets animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion;
#pragma mark Configuring How the User Interacts with the Map
@@ -784,9 +857,6 @@ MGL_EXPORT IB_DESIGNABLE
annotation is *not* centered within the viewport.
@param annotation The annotation object to select.
-
- @note In versions prior to `4.0.0` selecting an offscreen annotation did not
- change the camera.
*/
- (void)selectAnnotation:(id <MGLAnnotation>)annotation;
diff --git a/platform/macos/src/MGLMapView.mm b/platform/macos/src/MGLMapView.mm
index 0a1ec5516a..3c9647571e 100644
--- a/platform/macos/src/MGLMapView.mm
+++ b/platform/macos/src/MGLMapView.mm
@@ -978,6 +978,14 @@ public:
self.needsDisplay = YES;
}
+- (BOOL)shouldRemoveStyleImage:(NSString *)imageName {
+ if ([self.delegate respondsToSelector:@selector(mapView:shouldRemoveStyleImage:)]) {
+ return [self.delegate mapView:self shouldRemoveStyleImage:imageName];
+ }
+
+ return YES;
+}
+
#pragma mark Printing
- (void)print:(__unused id)sender {
@@ -1010,13 +1018,26 @@ public:
}
- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate animated:(BOOL)animated {
+ [self setCenterCoordinate:centerCoordinate animated:animated completionHandler:nil];
+}
+
+- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion {
MGLLogDebug(@"Setting centerCoordinate: %@ animated: %@", MGLStringFromCLLocationCoordinate2D(centerCoordinate), MGLStringFromBOOL(animated));
+ mbgl::AnimationOptions animationOptions = MGLDurationFromTimeInterval(animated ? MGLAnimationDuration : 0);
+ animationOptions.transitionFinishFn = ^() {
+ [self didChangeValueForKey:@"centerCoordinate"];
+ if (completion) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion();
+ });
+ }
+ };
+
[self willChangeValueForKey:@"centerCoordinate"];
_mbglMap->easeTo(mbgl::CameraOptions()
.withCenter(MGLLatLngFromLocationCoordinate2D(centerCoordinate))
.withPadding(MGLEdgeInsetsFromNSEdgeInsets(self.contentInsets)),
- MGLDurationFromTimeInterval(animated ? MGLAnimationDuration : 0));
- [self didChangeValueForKey:@"centerCoordinate"];
+ animationOptions);
}
- (void)offsetCenterCoordinateBy:(NSPoint)delta animated:(BOOL)animated {
@@ -1296,6 +1317,10 @@ public:
}
- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(NSEdgeInsets)insets animated:(BOOL)animated {
+ [self setVisibleCoordinateBounds:bounds edgePadding:insets animated:animated completionHandler:nil];
+}
+
+- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(NSEdgeInsets)insets animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion {
_mbglMap->cancelTransitions();
mbgl::EdgeInsets padding = MGLEdgeInsetsFromNSEdgeInsets(insets);
@@ -1308,12 +1333,18 @@ public:
MGLMapCamera *camera = [self cameraForCameraOptions:cameraOptions];
if ([self.camera isEqualToMapCamera:camera]) {
+ completion();
return;
}
[self willChangeValueForKey:@"visibleCoordinateBounds"];
animationOptions.transitionFinishFn = ^() {
[self didChangeValueForKey:@"visibleCoordinateBounds"];
+ if (completion) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion();
+ });
+ }
};
_mbglMap->easeTo(cameraOptions, animationOptions);
}
@@ -1408,13 +1439,18 @@ public:
}
- (void)setContentInsets:(NSEdgeInsets)contentInsets {
- MGLLogDebug(@"Setting contentInset: %@", MGLStringFromNSEdgeInsets(contentInsets));
- [self setContentInsets:contentInsets animated:NO];
+ [self setContentInsets:contentInsets animated:NO completionHandler:nil];
}
- (void)setContentInsets:(NSEdgeInsets)contentInsets animated:(BOOL)animated {
-
+ [self setContentInsets:contentInsets animated:animated completionHandler:nil];
+}
+
+- (void)setContentInsets:(NSEdgeInsets)contentInsets animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion {
if (NSEdgeInsetsEqual(contentInsets, self.contentInsets)) {
+ if (completion) {
+ completion();
+ }
return;
}
MGLLogDebug(@"Setting contentInset: %@ animated:", MGLStringFromNSEdgeInsets(contentInsets), MGLStringFromBOOL(animated));
@@ -1423,7 +1459,7 @@ public:
// content insets.
CLLocationCoordinate2D oldCenter = self.centerCoordinate;
_contentInsets = contentInsets;
- [self setCenterCoordinate:oldCenter animated:animated];
+ [self setCenterCoordinate:oldCenter animated:animated completionHandler:completion];
}
#pragma mark Mouse events and gestures
@@ -2465,25 +2501,31 @@ public:
}
- (void)showAnnotations:(NSArray<id <MGLAnnotation>> *)annotations edgePadding:(NSEdgeInsets)insets animated:(BOOL)animated {
- if ( ! annotations || ! annotations.count) return;
+ [self showAnnotations:annotations edgePadding:insets animated:animated completionHandler:nil];
+}
+
+- (void)showAnnotations:(NSArray<id <MGLAnnotation>> *)annotations edgePadding:(NSEdgeInsets)insets animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion {
+ if (!annotations.count) {
+ if (completion) {
+ completion();
+ }
+ return;
+ }
mbgl::LatLngBounds bounds = mbgl::LatLngBounds::empty();
- for (id <MGLAnnotation> annotation in annotations)
- {
- if ([annotation conformsToProtocol:@protocol(MGLOverlay)])
- {
+ for (id <MGLAnnotation> annotation in annotations) {
+ if ([annotation conformsToProtocol:@protocol(MGLOverlay)]) {
bounds.extend(MGLLatLngBoundsFromCoordinateBounds(((id <MGLOverlay>)annotation).overlayBounds));
- }
- else
- {
+ } else {
bounds.extend(MGLLatLngFromLocationCoordinate2D(annotation.coordinate));
}
}
[self setVisibleCoordinateBounds:MGLCoordinateBoundsFromLatLngBounds(bounds)
edgePadding:insets
- animated:animated];
+ animated:animated
+ completionHandler:completion];
}
/// Returns a popover detailing the annotation.
diff --git a/platform/macos/src/MGLMapViewDelegate.h b/platform/macos/src/MGLMapViewDelegate.h
index 1de4b47eb7..098164cd75 100644
--- a/platform/macos/src/MGLMapViewDelegate.h
+++ b/platform/macos/src/MGLMapViewDelegate.h
@@ -183,6 +183,20 @@ NS_ASSUME_NONNULL_BEGIN
- (nullable NSImage *)mapView:(MGLMapView *)mapView didFailToLoadImage:(NSString *)imageName;
+/**
+ Asks the delegate whether the map view should evict cached images.
+
+ This method is called in two scenarios: when the cumulative size of unused images
+ exceeds the cache size or when the last tile that includes the image is removed from
+ memory.
+
+ @param mapView The map view that is evicting the image.
+ @param imageName The image name that is going to be removed.
+ @return A Boolean value indicating whether the map view should evict
+ the cached image.
+ */
+- (BOOL)mapView:(MGLMapView *)mapView shouldRemoveStyleImage:(NSString *)imageName;
+
#pragma mark Managing the Appearance of Annotations
/**
diff --git a/platform/macos/src/MGLMapView_Private.h b/platform/macos/src/MGLMapView_Private.h
index afd7cf2422..3d9b36c30a 100644
--- a/platform/macos/src/MGLMapView_Private.h
+++ b/platform/macos/src/MGLMapView_Private.h
@@ -45,6 +45,7 @@ namespace mbgl {
- (void)mapViewDidBecomeIdle;
- (void)mapViewDidFinishLoadingStyle;
- (void)sourceDidChange:(nonnull MGLSource *)source;
+- (BOOL)shouldRemoveStyleImage:(nonnull NSString *)imageName;
/// Asynchronously render a frame of the map.
- (void)setNeedsRerender;
diff --git a/platform/macos/test/MGLMapViewDelegateIntegrationTests.swift b/platform/macos/test/MGLMapViewDelegateIntegrationTests.swift
index 6c37017be6..90a777e379 100644
--- a/platform/macos/test/MGLMapViewDelegateIntegrationTests.swift
+++ b/platform/macos/test/MGLMapViewDelegateIntegrationTests.swift
@@ -59,4 +59,5 @@ extension MGLMapViewDelegateIntegrationTests: MGLMapViewDelegate {
func mapView(_ mapView: MGLMapView, calloutViewControllerFor annotation: MGLAnnotation) -> NSViewController? { return nil }
+ func mapView(_ mapView: MGLMapView, shouldRemoveStyleImage imageName: String) -> Bool { return false }
}
diff --git a/platform/node/CHANGELOG.md b/platform/node/CHANGELOG.md
index d049944b6a..e03a64b049 100644
--- a/platform/node/CHANGELOG.md
+++ b/platform/node/CHANGELOG.md
@@ -3,6 +3,7 @@
- Add `crossSourceCollisions` map option, with default of `true`. When set to `false`, cross-source collision detection is disabled. ([#12820] (https://github.com/mapbox/mapbox-gl-native/issues/12820))
- Fixed bugs in coercion expression operators ("to-array" applied to empty arrays, "to-color" applied to colors, and "to-number" applied to null) [#12864](https://github.com/mapbox/mapbox-gl-native/pull/12864)
- Fixed an issue where fill and line layers would occasionally flicker on zoom ([#12982](https://github.com/mapbox/mapbox-gl-native/pull/12982))
+- Add an option to set whether or not an image should be treated as a SDF ([#15054](https://github.com/mapbox/mapbox-gl-native/issues/15054))
# 4.0.0
- Many new features and enhancements, including:
diff --git a/platform/node/README.md b/platform/node/README.md
index ac5bcd7e8d..7273c0813a 100644
--- a/platform/node/README.md
+++ b/platform/node/README.md
@@ -82,7 +82,7 @@ The first argument passed to `map.render` is an options object, all keys are opt
}
```
-When you are finished using a map object, you can call `map.release()` to permanently dispose the internal map resources. This is not necessary, but can be helpful to optimize resource usage (memory, file sockets) on a more granualar level than V8's garbage collector. Calling `map.release()` will prevent a map object from being used for any further render calls, but can be safely called as soon as the `map.render()` callback returns, as the returned pixel buffer will always be retained for the scope of the callback.
+When you are finished using a map object, you can call `map.release()` to permanently dispose the internal map resources. This is not necessary, but can be helpful to optimize resource usage (memory, file sockets) on a more granular level than V8's garbage collector. Calling `map.release()` will prevent a map object from being used for any further render calls, but can be safely called as soon as the `map.render()` callback returns, as the returned pixel buffer will always be retained for the scope of the callback.
## Implementing a file source
diff --git a/platform/node/src/node_expression.cpp b/platform/node/src/node_expression.cpp
index 4ea66124f8..041f60d029 100644
--- a/platform/node/src/node_expression.cpp
+++ b/platform/node/src/node_expression.cpp
@@ -42,7 +42,8 @@ type::Type parseType(v8::Local<v8::Object> type) {
{"object", type::Object},
{"color", type::Color},
{"value", type::Value},
- {"formatted", type::Formatted}
+ {"formatted", type::Formatted},
+ {"number-format", type::String}
};
v8::Local<v8::Value> v8kind = Nan::Get(type, Nan::New("kind").ToLocalChecked()).ToLocalChecked();
diff --git a/platform/node/src/node_map.cpp b/platform/node/src/node_map.cpp
index f88b6c6c79..641816dc00 100644
--- a/platform/node/src/node_map.cpp
+++ b/platform/node/src/node_map.cpp
@@ -544,7 +544,9 @@ void NodeMap::renderFinished() {
},
img.data.get()
).ToLocalChecked();
- img.data.release();
+ if (!pixels.IsEmpty()) {
+ img.data.release();
+ }
v8::Local<v8::Value> argv[] = {
Nan::Null(),
@@ -777,6 +779,11 @@ void NodeMap::AddImage(const Nan::FunctionCallbackInfo<v8::Value>& info) {
return Nan::ThrowTypeError("Max height and width is 1024");
}
+ bool sdf = false;
+ if (Nan::Get(optionObject, Nan::New("sdf").ToLocalChecked()).ToLocalChecked()->IsBoolean()) {
+ sdf = Nan::Get(optionObject, Nan::New("sdf").ToLocalChecked()).ToLocalChecked()->BooleanValue();
+ }
+
float pixelRatio = Nan::Get(optionObject, Nan::New("pixelRatio").ToLocalChecked()).ToLocalChecked()->NumberValue();
auto imageBuffer = Nan::To<v8::Object>(info[1]).ToLocalChecked()->ToObject();
@@ -792,7 +799,7 @@ void NodeMap::AddImage(const Nan::FunctionCallbackInfo<v8::Value>& info) {
mbgl::UnassociatedImage cImage({ imageWidth, imageHeight}, std::move(data));
mbgl::PremultipliedImage cPremultipliedImage = mbgl::util::premultiply(std::move(cImage));
- nodeMap->map->getStyle().addImage(std::make_unique<mbgl::style::Image>(*Nan::Utf8String(info[0]), std::move(cPremultipliedImage), pixelRatio));
+ nodeMap->map->getStyle().addImage(std::make_unique<mbgl::style::Image>(*Nan::Utf8String(info[0]), std::move(cPremultipliedImage), pixelRatio, sdf));
}
void NodeMap::RemoveImage(const Nan::FunctionCallbackInfo<v8::Value>& info) {
@@ -1244,20 +1251,19 @@ std::unique_ptr<mbgl::AsyncRequest> NodeFileSource::request(const mbgl::Resource
// *this while we're still executing code.
nodeMap->handle();
+ auto asyncRequest = std::make_unique<node_mbgl::NodeAsyncRequest>();
+
v8::Local<v8::Value> argv[] = {
Nan::New<v8::External>(nodeMap),
- Nan::New<v8::External>(&callback_)
+ Nan::New<v8::External>(&callback_),
+ Nan::New<v8::External>(asyncRequest.get()),
+ Nan::New(resource.url).ToLocalChecked(),
+ Nan::New<v8::Integer>(resource.kind)
};
- auto instance = Nan::NewInstance(Nan::New(node_mbgl::NodeRequest::constructor), 2, argv).ToLocalChecked();
-
- Nan::Set(instance, Nan::New("url").ToLocalChecked(), Nan::New(resource.url).ToLocalChecked());
- Nan::Set(instance, Nan::New("kind").ToLocalChecked(), Nan::New<v8::Integer>(resource.kind));
-
- auto req = Nan::ObjectWrap::Unwrap<node_mbgl::NodeRequest>(instance);
- req->Execute();
+ Nan::NewInstance(Nan::New(node_mbgl::NodeRequest::constructor), 5, argv).ToLocalChecked();
- return std::make_unique<node_mbgl::NodeRequest::NodeAsyncRequest>(req);
+ return asyncRequest;
}
} // namespace node_mbgl
diff --git a/platform/node/src/node_map.hpp b/platform/node/src/node_map.hpp
index d45dbec92b..486754c1c3 100644
--- a/platform/node/src/node_map.hpp
+++ b/platform/node/src/node_map.hpp
@@ -93,6 +93,7 @@ public:
struct NodeFileSource : public mbgl::FileSource {
NodeFileSource(NodeMap* nodeMap_) : nodeMap(nodeMap_) {}
+ ~NodeFileSource() {}
std::unique_ptr<mbgl::AsyncRequest> request(const mbgl::Resource&, mbgl::FileSource::Callback) final;
NodeMap* nodeMap;
};
diff --git a/platform/node/src/node_request.cpp b/platform/node/src/node_request.cpp
index 8c26d44583..78f50ea9ce 100644
--- a/platform/node/src/node_request.cpp
+++ b/platform/node/src/node_request.cpp
@@ -8,11 +8,11 @@
namespace node_mbgl {
NodeRequest::NodeRequest(
- NodeMap* target_,
- mbgl::FileSource::Callback callback_)
- : AsyncWorker(nullptr),
- target(target_),
- callback(std::move(callback_)) {
+ mbgl::FileSource::Callback callback_,
+ NodeAsyncRequest* asyncRequest_)
+ : callback(std::move(callback_)),
+ asyncRequest(asyncRequest_) {
+ asyncRequest->request = this;
}
NodeRequest::~NodeRequest() {
@@ -40,10 +40,16 @@ void NodeRequest::Init() {
void NodeRequest::New(const Nan::FunctionCallbackInfo<v8::Value>& info) {
auto target = reinterpret_cast<NodeMap*>(info[0].As<v8::External>()->Value());
auto callback = reinterpret_cast<mbgl::FileSource::Callback*>(info[1].As<v8::External>()->Value());
+ auto asyncRequest = reinterpret_cast<NodeAsyncRequest*>(info[2].As<v8::External>()->Value());
- auto request = new NodeRequest(target, *callback);
+ auto request = new NodeRequest(*callback, asyncRequest);
request->Wrap(info.This());
+ request->Ref();
+ Nan::Set(info.This(), Nan::New("url").ToLocalChecked(), info[3]);
+ Nan::Set(info.This(), Nan::New("kind").ToLocalChecked(), info[4]);
+ v8::Local<v8::Value> argv[] = { info.This() };
+ request->asyncResource->runInAsyncScope(Nan::To<v8::Object>(target->handle()->GetInternalField(1)).ToLocalChecked(), "request", 1, argv);
info.GetReturnValue().Set(info.This());
}
@@ -52,7 +58,9 @@ void NodeRequest::HandleCallback(const Nan::FunctionCallbackInfo<v8::Value>& inf
// Move out of the object so callback() can only be fired once.
auto callback = std::move(request->callback);
+ request->callback = {};
if (!callback) {
+ request->unref();
return info.GetReturnValue().SetUndefined();
}
@@ -65,12 +73,18 @@ void NodeRequest::HandleCallback(const Nan::FunctionCallbackInfo<v8::Value>& inf
auto msg = Nan::New("message").ToLocalChecked();
if (Nan::Has(err, msg).FromJust()) {
- request->SetErrorMessage(*Nan::Utf8String(
- Nan::Get(err, msg).ToLocalChecked()));
+ response.error = std::make_unique<mbgl::Response::Error>(
+ mbgl::Response::Error::Reason::Other,
+ *Nan::Utf8String(Nan::Get(err, msg).ToLocalChecked())
+ );
}
} else if (info[0]->IsString()) {
- request->SetErrorMessage(*Nan::Utf8String(info[0]));
+ response.error = std::make_unique<mbgl::Response::Error>(
+ mbgl::Response::Error::Reason::Other,
+ *Nan::Utf8String(info[0])
+ );
} else if (info.Length() < 2 || !info[1]->IsObject()) {
+ request->unref();
return Nan::ThrowTypeError("Second argument must be a response object");
} else {
auto res = Nan::To<v8::Object>(info[1]).ToLocalChecked();
@@ -104,43 +118,34 @@ void NodeRequest::HandleCallback(const Nan::FunctionCallbackInfo<v8::Value>& inf
node::Buffer::Length(data)
);
} else {
+ request->unref();
return Nan::ThrowTypeError("Response data must be a Buffer");
}
}
}
- if (request->ErrorMessage()) {
- response.error = std::make_unique<mbgl::Response::Error>(
- mbgl::Response::Error::Reason::Other,
- request->ErrorMessage()
- );
- }
-
// Send the response object to the NodeFileSource object
callback(response);
+ request->unref();
info.GetReturnValue().SetUndefined();
}
-void NodeRequest::Execute() {
- v8::Local<v8::Value> argv[] = { handle() };
-
- Nan::AsyncResource res("mbgl:execute");
- res.runInAsyncScope(Nan::To<v8::Object>(target->handle()->GetInternalField(1)).ToLocalChecked(), "request", 1, argv);
+void NodeRequest::unref() {
+ Nan::HandleScope scope;
+ delete asyncResource;
+ asyncResource = nullptr;
+ Unref();
}
-NodeRequest::NodeAsyncRequest::NodeAsyncRequest(NodeRequest* request_) : request(request_) {
- assert(request);
+NodeAsyncRequest::NodeAsyncRequest() : request(nullptr) {}
- // Make sure the JS object has a pointer to this so that it can remove
- // its pointer in the destructor
- request->asyncRequest = this;
-}
-
-NodeRequest::NodeAsyncRequest::~NodeAsyncRequest() {
+NodeAsyncRequest::~NodeAsyncRequest() {
if (request) {
// Remove the callback function because the AsyncRequest was
// canceled and we are no longer interested in the result.
- request->callback = {};
+ if (request->callback) {
+ request->callback = {};
+ }
request->asyncRequest = nullptr;
}
}
diff --git a/platform/node/src/node_request.hpp b/platform/node/src/node_request.hpp
index 7d7679a3c7..830d262b40 100644
--- a/platform/node/src/node_request.hpp
+++ b/platform/node/src/node_request.hpp
@@ -11,18 +11,19 @@
namespace node_mbgl {
-class NodeMap;
+class NodeRequest;
+
+struct NodeAsyncRequest : public mbgl::AsyncRequest {
+ NodeAsyncRequest();
+ ~NodeAsyncRequest() override;
+ NodeRequest* request;
+};
+
+class NodeRequest : public Nan::ObjectWrap {
-class NodeRequest : public Nan::ObjectWrap,
- public Nan::AsyncWorker {
public:
- struct NodeAsyncRequest : public mbgl::AsyncRequest {
- NodeAsyncRequest(NodeRequest*);
- ~NodeAsyncRequest() override;
- NodeRequest* request;
- };
- NodeRequest(NodeMap*, mbgl::FileSource::Callback);
+ NodeRequest(mbgl::FileSource::Callback, NodeAsyncRequest*);
~NodeRequest();
static Nan::Persistent<v8::Function> constructor;
@@ -32,12 +33,11 @@ public:
static void New(const Nan::FunctionCallbackInfo<v8::Value>&);
static void HandleCallback(const Nan::FunctionCallbackInfo<v8::Value>&);
- void Execute();
+ void unref();
-private:
- NodeMap* target;
mbgl::FileSource::Callback callback;
- NodeAsyncRequest* asyncRequest = nullptr;
+ NodeAsyncRequest* asyncRequest;
+ Nan::AsyncResource* asyncResource = new Nan::AsyncResource("mbgl:execute");
};
}
diff --git a/platform/node/test/ignores.json b/platform/node/test/ignores.json
index b3fb4b5a51..63b9764f22 100644
--- a/platform/node/test/ignores.json
+++ b/platform/node/test/ignores.json
@@ -77,7 +77,6 @@
"render-tests/regressions/mapbox-gl-js#7271": "https://github.com/mapbox/mapbox-gl-native/issues/12888",
"render-tests/regressions/mapbox-gl-js#7302": "skip - js specific",
"render-tests/regressions/mapbox-gl-native#7357": "https://github.com/mapbox/mapbox-gl-native/issues/7357",
- "render-tests/runtime-styling/image-add-sdf": "https://github.com/mapbox/mapbox-gl-native/issues/9847",
"render-tests/runtime-styling/paint-property-fill-flat-to-extrude": "https://github.com/mapbox/mapbox-gl-native/issues/6745",
"render-tests/runtime-styling/set-style-paint-property-fill-flat-to-extrude": "https://github.com/mapbox/mapbox-gl-native/issues/6745",
"render-tests/symbol-cross-fade/chinese": "https://github.com/mapbox/mapbox-gl-native/issues/10619",
diff --git a/platform/qt/qt.cmake b/platform/qt/qt.cmake
index 48523057c9..33acb7a030 100644
--- a/platform/qt/qt.cmake
+++ b/platform/qt/qt.cmake
@@ -35,6 +35,7 @@ set(MBGL_QT_CORE_FILES
PRIVATE platform/qt/src/timer_impl.hpp
PRIVATE platform/qt/src/utf.cpp
PRIVATE platform/qt/src/gl_functions.cpp
+ PRIVATE platform/qt/src/format_number.cpp
PRIVATE platform/default/src/mbgl/text/collator.cpp
PRIVATE platform/default/src/mbgl/text/unaccent.cpp
diff --git a/platform/qt/src/format_number.cpp b/platform/qt/src/format_number.cpp
new file mode 100644
index 0000000000..b6fe3558e6
--- /dev/null
+++ b/platform/qt/src/format_number.cpp
@@ -0,0 +1,26 @@
+#include <mbgl/util/platform.hpp>
+
+#include <QLocale>
+#include <QString>
+
+namespace mbgl {
+namespace platform {
+
+std::string formatNumber(double number, const std::string& localeId, const std::string& currency,
+ uint8_t minFractionDigits, uint8_t maxFractionDigits) {
+
+ QString formatted;
+ // Qt Locale::toString() API takes only one precision argument
+ (void)minFractionDigits;
+ QLocale locale = QLocale(QString::fromStdString(localeId));
+
+ if (!currency.empty()) {
+ formatted = locale.toCurrencyString(number);
+ } else {
+ formatted = locale.toString(number, 'f', maxFractionDigits);
+ }
+ return formatted.toStdString();
+}
+
+} // namespace platform
+} // namespace mbgl
diff --git a/render-test/filesystem.hpp b/render-test/filesystem.hpp
new file mode 100644
index 0000000000..cee7e9d911
--- /dev/null
+++ b/render-test/filesystem.hpp
@@ -0,0 +1,9 @@
+#pragma once
+
+#include <ghc/filesystem.hpp>
+
+namespace mbgl {
+
+namespace filesystem = ghc::filesystem;
+
+} // namespace mbgl
diff --git a/render-test/main.cpp b/render-test/main.cpp
new file mode 100644
index 0000000000..35f0cdea30
--- /dev/null
+++ b/render-test/main.cpp
@@ -0,0 +1,154 @@
+#include <mbgl/util/run_loop.hpp>
+#include <mbgl/util/io.hpp>
+
+#include "filesystem.hpp"
+#include "metadata.hpp"
+#include "parser.hpp"
+#include "runner.hpp"
+
+#include <random>
+
+#define ANSI_COLOR_RED "\x1b[31m"
+#define ANSI_COLOR_GREEN "\x1b[32m"
+#define ANSI_COLOR_YELLOW "\x1b[33m"
+#define ANSI_COLOR_BLUE "\x1b[34m"
+#define ANSI_COLOR_MAGENTA "\x1b[35m"
+#define ANSI_COLOR_CYAN "\x1b[36m"
+#define ANSI_COLOR_GRAY "\x1b[37m"
+#define ANSI_COLOR_LIGHT_GRAY "\x1b[90m"
+#define ANSI_COLOR_RESET "\x1b[0m"
+
+int main(int argc, char** argv) {
+ bool recycleMap;
+ bool shuffle;
+ uint32_t seed;
+ std::string testRootPath;
+ std::vector<std::string> ids;
+
+ std::tie(recycleMap, shuffle, seed, testRootPath, ids) = parseArguments(argc, argv);
+ const std::string::size_type rootLength = testRootPath.length();
+
+ const auto ignores = parseIgnores();
+
+ // Recursively traverse through the test paths and collect test directories containing "style.json".
+ std::vector<mbgl::filesystem::path> testPaths;
+ for (const auto& id : ids) {
+ for (auto& testPath : mbgl::filesystem::recursive_directory_iterator(mbgl::filesystem::path(id))) {
+ if (testPath.path().filename() == "style.json") {
+ testPaths.push_back(testPath);
+ }
+ }
+ }
+
+ if (shuffle) {
+ printf(ANSI_COLOR_YELLOW "Shuffle seed: %d" ANSI_COLOR_RESET "\n", seed);
+
+ std::seed_seq sequence { seed };
+ std::mt19937 shuffler(sequence);
+ std::shuffle(testPaths.begin(), testPaths.end(), shuffler);
+ }
+
+ mbgl::util::RunLoop runLoop;
+ TestRunner runner;
+
+ std::vector<TestMetadata> metadatas;
+ metadatas.reserve(testPaths.size());
+
+ TestStatistics stats;
+
+ for (auto& testPath : testPaths) {
+ TestMetadata metadata = parseTestMetadata(testPath);
+
+ if (!recycleMap) {
+ runner.reset();
+ }
+
+ std::string& id = metadata.id;
+ std::string& status = metadata.status;
+ std::string& color = metadata.color;
+
+ id = testPath.remove_filename().string();
+ id = id.substr(rootLength + 1, id.length() - rootLength - 2);
+
+ bool shouldIgnore = false;
+ std::string ignoreReason;
+
+ const std::string ignoreName = "render-tests/" + id;
+ const auto it = std::find_if(ignores.cbegin(), ignores.cend(), [&ignoreName](auto pair) { return pair.first == ignoreName; });
+ if (it != ignores.end()) {
+ shouldIgnore = true;
+ ignoreReason = it->second;
+ if (ignoreReason.rfind("skip", 0) == 0) {
+ printf(ANSI_COLOR_GRAY "* skipped %s (%s)" ANSI_COLOR_RESET "\n", id.c_str(), ignoreReason.c_str());
+ continue;
+ }
+ }
+
+ bool errored = !metadata.errorMessage.empty();
+ if (!errored) {
+ errored = runner.run(metadata) && !metadata.errorMessage.empty();
+ }
+
+ bool passed = !errored && !metadata.diff.empty() && metadata.difference <= metadata.allowed;
+
+ if (shouldIgnore) {
+ if (passed) {
+ status = "ignored passed";
+ color = "#E8A408";
+ stats.ignorePassedTests++;
+ printf(ANSI_COLOR_YELLOW "* ignore %s (%s)" ANSI_COLOR_RESET "\n", id.c_str(), ignoreReason.c_str());
+ } else {
+ status = "ignored failed";
+ color = "#9E9E9E";
+ stats.ignoreFailedTests++;
+ printf(ANSI_COLOR_LIGHT_GRAY "* ignore %s (%s)" ANSI_COLOR_RESET "\n", id.c_str(), ignoreReason.c_str());
+ }
+ } else {
+ if (passed) {
+ status = "passed";
+ color = "green";
+ stats.passedTests++;
+ printf(ANSI_COLOR_GREEN "* passed %s" ANSI_COLOR_RESET "\n", id.c_str());
+ } else if (errored) {
+ status = "errored";
+ color = "red";
+ stats.erroredTests++;
+ printf(ANSI_COLOR_RED "* errored %s" ANSI_COLOR_RESET "\n", id.c_str());
+ } else {
+ status = "failed";
+ color = "red";
+ stats.failedTests++;
+ printf(ANSI_COLOR_RED "* failed %s" ANSI_COLOR_RESET "\n", id.c_str());
+ }
+ }
+
+ metadatas.push_back(std::move(metadata));
+ }
+
+ std::string resultsHTML = createResultPage(stats, metadatas, shuffle, seed);
+ mbgl::util::write_file(testRootPath + "/index.html", resultsHTML);
+
+ const uint32_t count = stats.erroredTests + stats.failedTests +
+ stats.ignoreFailedTests + stats.ignorePassedTests +
+ stats.passedTests;
+
+ if (stats.passedTests) {
+ printf(ANSI_COLOR_GREEN "%u passed (%.1lf%%)" ANSI_COLOR_RESET "\n", stats.passedTests, 100.0 * stats.passedTests / count);
+ }
+ if (stats.ignorePassedTests) {
+ printf(ANSI_COLOR_YELLOW "%u passed but were ignored (%.1lf%%)" ANSI_COLOR_RESET "\n", stats.ignorePassedTests, 100.0 * stats.ignorePassedTests / count);
+ }
+ if (stats.ignoreFailedTests) {
+ printf(ANSI_COLOR_LIGHT_GRAY "%u ignored (%.1lf%%)" ANSI_COLOR_RESET "\n", stats.ignoreFailedTests, 100.0 * stats.ignoreFailedTests / count);
+ }
+ if (stats.failedTests) {
+ printf(ANSI_COLOR_RED "%u failed (%.1lf%%)" ANSI_COLOR_RESET "\n", stats.failedTests, 100.0 * stats.failedTests / count);
+ }
+ if (stats.erroredTests) {
+ printf(ANSI_COLOR_RED "%u errored (%.1lf%%)" ANSI_COLOR_RESET "\n", stats.erroredTests, 100.0 * stats.erroredTests / count);
+ }
+
+ printf("Results at: %s%s\n", testRootPath.c_str(), "/index.html");
+
+ return stats.failedTests + stats.erroredTests == 0 ? 0 : 1;
+}
diff --git a/render-test/metadata.hpp b/render-test/metadata.hpp
new file mode 100644
index 0000000000..4be83a5436
--- /dev/null
+++ b/render-test/metadata.hpp
@@ -0,0 +1,52 @@
+#pragma once
+
+#include <mbgl/util/rapidjson.hpp>
+#include <mbgl/util/size.hpp>
+
+#include <mbgl/map/mode.hpp>
+
+#include "filesystem.hpp"
+
+struct TestStatistics {
+ TestStatistics() = default;
+
+ uint32_t ignoreFailedTests = 0;
+ uint32_t ignorePassedTests = 0;
+ uint32_t erroredTests = 0;
+ uint32_t failedTests = 0;
+ uint32_t passedTests = 0;
+};
+
+struct TestMetadata {
+ TestMetadata() = default;
+
+ mbgl::filesystem::path path;
+ mbgl::JSDocument document;
+
+ mbgl::Size size{ 512u, 512u };
+ float pixelRatio = 1.0f;
+ double allowed = 0.00015; // diff
+ std::string description;
+ mbgl::MapMode mapMode = mbgl::MapMode::Static;
+ mbgl::MapDebugOptions debug = mbgl::MapDebugOptions::NoDebug;
+ bool crossSourceCollisions = true;
+ bool axonometric = false;
+ double xSkew = 0.0;
+ double ySkew = 1.0;
+
+ // TODO
+ uint32_t fadeDuration = 0;
+ bool addFakeCanvas = false;
+
+ // HTML
+ std::string id;
+ std::string status;
+ std::string color;
+
+ std::string actual;
+ std::string expected;
+ std::string diff;
+
+ std::string errorMessage;
+ double difference = 0.0;
+}; \ No newline at end of file
diff --git a/render-test/parser.cpp b/render-test/parser.cpp
new file mode 100644
index 0000000000..089a5c45c9
--- /dev/null
+++ b/render-test/parser.cpp
@@ -0,0 +1,572 @@
+#include <mbgl/util/logging.hpp>
+#include <mbgl/util/io.hpp>
+#include <mbgl/util/rapidjson.hpp>
+#include <mbgl/util/string.hpp>
+
+#include <args.hxx>
+
+#include <rapidjson/stringbuffer.h>
+#include <rapidjson/writer.h>
+
+#include <boost/archive/iterators/base64_from_binary.hpp>
+#include <boost/archive/iterators/insert_linebreaks.hpp>
+#include <boost/archive/iterators/transform_width.hpp>
+#include <boost/archive/iterators/ostream_iterator.hpp>
+
+#include "parser.hpp"
+#include "metadata.hpp"
+
+#include <sstream>
+#include <regex>
+
+namespace {
+
+const char* resultsStyle = R"HTML(
+<style>
+ body { font: 18px/1.2 -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif; padding: 10px; }
+ h1 { font-size: 32px; margin-bottom: 0; }
+ button { vertical-align: middle; }
+ h2 { font-size: 24px; font-weight: normal; margin: 10px 0 10px; line-height: 1; }
+ img { margin: 0 10px 10px 0; border: 1px dotted #ccc; }
+ .stats { margin-top: 10px; }
+ .test { border-bottom: 1px dotted #bbb; padding-bottom: 5px; }
+ .tests { border-top: 1px dotted #bbb; margin-top: 10px; }
+ .diff { color: #777; }
+ .test p, .test pre { margin: 0 0 10px; }
+ .test pre { font-size: 14px; }
+ .label { color: white; font-size: 18px; padding: 2px 6px 3px; border-radius: 3px; margin-right: 3px; vertical-align: bottom; display: inline-block; }
+ .hide { display: none; }
+</style>
+)HTML";
+
+const char* resultsScript = R"HTML(
+<script>
+document.addEventListener('mouseover', handleHover);
+document.addEventListener('mouseout', handleHover);
+
+function handleHover(e) {
+ var el = e.target;
+ if (el.tagName === 'IMG' && el.dataset.altSrc) {
+ var tmp = el.src;
+ el.src = el.dataset.altSrc;
+ el.dataset.altSrc = tmp;
+ }
+}
+
+document.getElementById('toggle-passed').addEventListener('click', function (e) {
+ for (const row of document.querySelectorAll('.test.passed')) {
+ row.classList.toggle('hide');
+ }
+});
+document.getElementById('toggle-ignored').addEventListener('click', function (e) {
+ for (const row of document.querySelectorAll('.test.ignored')) {
+ row.classList.toggle('hide');
+ }
+});
+document.getElementById('toggle-sequence').addEventListener('click', function (e) {
+ document.getElementById('test-sequence').classList.toggle('hide');
+});
+</script>
+)HTML";
+
+const char* resultsHeaderButtons = R"HTML(
+ <button id='toggle-sequence'>Toggle test sequence</button>
+ <button id='toggle-passed'>Toggle passed tests</button>
+ <button id='toggle-ignored'>Toggle ignored tests</button>
+</h1>
+)HTML";
+
+std::string removeURLArguments(const std::string &url) {
+ std::string::size_type index = url.find('?');
+ if (index != std::string::npos) {
+ return url.substr(0, index);
+ }
+ return url;
+}
+
+std::string prependFileScheme(const std::string &url) {
+ static const std::string fileScheme("file://");
+ return fileScheme + url;
+}
+
+mbgl::optional<std::string> getVendorPath(const std::string& url, const std::regex& regex, bool glyphsPath = false) {
+ static const mbgl::filesystem::path vendorPath(std::string(TEST_RUNNER_ROOT_PATH) + "/vendor/");
+
+ mbgl::filesystem::path file = std::regex_replace(url, regex, vendorPath.string());
+ if (mbgl::filesystem::exists(file.parent_path())) {
+ return removeURLArguments(file.string());
+ }
+
+ if (glyphsPath && mbgl::filesystem::exists(file.parent_path().parent_path())) {
+ return removeURLArguments(file.string());
+ }
+
+ return {};
+}
+
+mbgl::optional<std::string> getIntegrationPath(const std::string& url, const std::string& parent, const std::regex& regex, bool glyphsPath = false) {
+ static const mbgl::filesystem::path integrationPath(std::string(TEST_RUNNER_ROOT_PATH) + "/mapbox-gl-js/test/integration/");
+
+ mbgl::filesystem::path file = std::regex_replace(url, regex, integrationPath.string() + parent);
+ if (mbgl::filesystem::exists(file.parent_path())) {
+ return removeURLArguments(file.string());
+ }
+
+ if (glyphsPath && mbgl::filesystem::exists(file.parent_path().parent_path())) {
+ return removeURLArguments(file.string());
+ }
+
+ return {};
+}
+
+mbgl::optional<std::string> localizeLocalURL(const std::string& url, bool glyphsPath = false) {
+ static const std::regex regex { "local://" };
+ if (auto vendorPath = getVendorPath(url, regex, glyphsPath)) {
+ return vendorPath;
+ } else {
+ return getIntegrationPath(url, "", regex, glyphsPath);
+ }
+}
+
+mbgl::optional<std::string> localizeHttpURL(const std::string& url) {
+ static const std::regex regex { "http://localhost:2900" };
+ if (auto vendorPath = getVendorPath(url, regex)) {
+ return vendorPath;
+ } else {
+ return getIntegrationPath(url, "", regex);
+ }
+}
+
+mbgl::optional<std::string> localizeMapboxSpriteURL(const std::string& url) {
+ static const std::regex regex { "mapbox://" };
+ return getIntegrationPath(url, "", regex);
+}
+
+mbgl::optional<std::string> localizeMapboxFontsURL(const std::string& url) {
+ static const std::regex regex { "mapbox://fonts" };
+ return getIntegrationPath(url, "glyphs/", regex, true);
+}
+
+mbgl::optional<std::string> localizeMapboxTilesURL(const std::string& url) {
+ static const std::regex regex { "mapbox://" };
+ if (auto vendorPath = getVendorPath(url, regex)) {
+ return vendorPath;
+ } else {
+ return getIntegrationPath(url, "tiles/", regex);
+ }
+}
+
+mbgl::optional<std::string> localizeMapboxTilesetURL(const std::string& url) {
+ static const std::regex regex { "mapbox://" };
+ return getIntegrationPath(url, "tilesets/", regex);
+}
+
+} // namespace
+
+JSONReply readJson(const mbgl::filesystem::path& jsonPath) {
+ auto maybeJSON = mbgl::util::readFile(jsonPath);
+ if (!maybeJSON) {
+ return { std::string("Unable to open file ") + jsonPath.string() };
+ }
+
+ mbgl::JSDocument document;
+ document.Parse<0>(*maybeJSON);
+ if (document.HasParseError()) {
+ return { mbgl::formatJSONParseError(document) };
+ }
+
+ return { std::move(document) };
+}
+
+std::string serializeJsonValue(const mbgl::JSValue& value) {
+ rapidjson::StringBuffer buffer;
+ buffer.Clear();
+ rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
+ value.Accept(writer);
+ return buffer.GetString();
+}
+
+std::vector<std::string> readExpectedEntries(const mbgl::filesystem::path& base) {
+ static const std::regex regex(".*expected.*.png");
+
+ std::vector<std::string> expectedImages;
+ for (const auto& entry : mbgl::filesystem::directory_iterator(base)) {
+ if (entry.is_regular_file()) {
+ const std::string path = entry.path().string();
+ if (std::regex_match(path, regex)) {
+ expectedImages.emplace_back(std::move(path));
+ }
+ }
+ }
+ return expectedImages;
+}
+
+
+ArgumentsTuple parseArguments(int argc, char** argv) {
+ args::ArgumentParser argumentParser("Mapbox GL Test Runner");
+
+ args::HelpFlag helpFlag(argumentParser, "help", "Display this help menu", { 'h', "help" });
+
+ args::Flag recycleMapFlag(argumentParser, "recycle map", "Toggle reusing the map object",
+ { 'r', "recycle-map" });
+ args::Flag shuffleFlag(argumentParser, "shuffle", "Toggle shuffling the tests order",
+ { 's', "shuffle" });
+ args::ValueFlag<uint32_t> seedValue(argumentParser, "seed", "Shuffle seed (default: random)",
+ { "seed" });
+ args::ValueFlag<std::string> testPathValue(argumentParser, "rootPath", "Test root rootPath",
+ { 'p', "rootPath" });
+ args::PositionalList<std::string> testNameValues(argumentParser, "URL", "Test name(s)");
+
+ try {
+ argumentParser.ParseCLI(argc, argv);
+ } catch (const args::Help&) {
+ std::ostringstream stream;
+ stream << argumentParser;
+ mbgl::Log::Info(mbgl::Event::General, stream.str());
+ exit(0);
+ } catch (const args::ParseError& e) {
+ std::ostringstream stream;
+ stream << argumentParser;
+ mbgl::Log::Info(mbgl::Event::General, stream.str());
+ mbgl::Log::Error(mbgl::Event::General, e.what());
+ exit(1);
+ } catch (const args::ValidationError& e) {
+ std::ostringstream stream;
+ stream << argumentParser;
+ mbgl::Log::Info(mbgl::Event::General, stream.str());
+ mbgl::Log::Error(mbgl::Event::General, e.what());
+ exit(2);
+ }
+
+ const std::string testDefaultPath =
+ std::string(TEST_RUNNER_ROOT_PATH).append("/mapbox-gl-js/test/integration/render-tests");
+
+ std::vector<std::string> ids;
+ for (const auto& id : args::get(testNameValues)) {
+ ids.emplace_back(testDefaultPath + "/" + id);
+ }
+
+ if (ids.empty()) {
+ ids.emplace_back(testDefaultPath);
+ }
+
+ return ArgumentsTuple {
+ recycleMapFlag ? args::get(recycleMapFlag) : false,
+ shuffleFlag ? args::get(shuffleFlag) : false, seedValue ? args::get(seedValue) : 1u,
+ testPathValue ? args::get(testPathValue) : testDefaultPath, ids
+ };
+}
+
+std::vector<std::pair<std::string, std::string>> parseIgnores() {
+ std::vector<std::pair<std::string, std::string>> ignores;
+
+ auto path = mbgl::filesystem::path(TEST_RUNNER_ROOT_PATH).append("platform/node/test/ignores.json");
+
+ auto maybeIgnores = readJson(path.string());
+ if (maybeIgnores.is<mbgl::JSDocument>()) {
+ for (const auto& property : maybeIgnores.get<mbgl::JSDocument>().GetObject()) {
+ const std::string ignore = { property.name.GetString(),
+ property.name.GetStringLength() };
+ const std::string reason = { property.value.GetString(),
+ property.value.GetStringLength() };
+ ignores.emplace_back(std::make_pair(ignore, reason));
+ }
+ }
+
+ return ignores;
+}
+
+TestMetadata parseTestMetadata(const mbgl::filesystem::path& path) {
+ TestMetadata metadata;
+ metadata.path = path;
+
+ auto maybeJson = readJson(path.string());
+ if (!maybeJson.is<mbgl::JSDocument>()) { // NOLINT
+ metadata.errorMessage = std::string("Unable to parse: ") + path.string();
+ return metadata;
+ }
+
+ metadata.document = std::move(maybeJson.get<mbgl::JSDocument>());
+ localizeStyleURLs(metadata.document, metadata.document);
+
+ if (!metadata.document.HasMember("metadata")) {
+ mbgl::Log::Warning(mbgl::Event::ParseStyle, "Style has no 'metadata': %s",
+ path.c_str());
+ return metadata;
+ }
+
+ const mbgl::JSValue& metadataValue = metadata.document["metadata"];
+ if (!metadataValue.HasMember("test")) {
+ mbgl::Log::Warning(mbgl::Event::ParseStyle, "Style has no 'metadata.test': %s",
+ path.c_str());
+ return metadata;
+ }
+
+ const mbgl::JSValue& testValue = metadataValue["test"];
+
+ if (testValue.HasMember("width")) {
+ assert(testValue["width"].IsNumber());
+ metadata.size.width = testValue["width"].GetInt();
+ }
+
+ if (testValue.HasMember("height")) {
+ assert(testValue["height"].IsNumber());
+ metadata.size.height = testValue["height"].GetInt();
+ }
+
+ if (testValue.HasMember("pixelRatio")) {
+ assert(testValue["pixelRatio"].IsNumber());
+ metadata.pixelRatio = testValue["pixelRatio"].GetFloat();
+ }
+
+ if (testValue.HasMember("allowed")) {
+ assert(testValue["allowed"].IsNumber());
+ metadata.allowed = testValue["allowed"].GetDouble();
+ }
+
+ if (testValue.HasMember("description")) {
+ assert(testValue["description"].IsString());
+ metadata.description = std::string{ testValue["description"].GetString(),
+ testValue["description"].GetStringLength() };
+ }
+
+ if (testValue.HasMember("mapMode")) {
+ assert(testValue["mapMode"].IsString());
+ metadata.mapMode = testValue["mapMode"].GetString() == std::string("tile") ? mbgl::MapMode::Tile : mbgl::MapMode::Static;
+ }
+
+ // Test operations handled in runner.cpp.
+
+ if (testValue.HasMember("debug")) {
+ metadata.debug |= mbgl::MapDebugOptions::TileBorders;
+ }
+
+ if (testValue.HasMember("collisionDebug")) {
+ metadata.debug |= mbgl::MapDebugOptions::Collision;
+ }
+
+ if (testValue.HasMember("showOverdrawInspector")) {
+ metadata.debug |= mbgl::MapDebugOptions::Overdraw;
+ }
+
+ if (testValue.HasMember("crossSourceCollisions")) {
+ assert(testValue["crossSourceCollisions"].IsBool());
+ metadata.crossSourceCollisions = testValue["crossSourceCollisions"].GetBool();
+ }
+
+ if (testValue.HasMember("axonometric")) {
+ assert(testValue["axonometric"].IsBool());
+ metadata.axonometric = testValue["axonometric"].GetBool();
+ }
+
+ if (testValue.HasMember("skew")) {
+ assert(testValue["skew"].IsArray());
+ metadata.xSkew = testValue["skew"][0].GetDouble();
+ metadata.ySkew = testValue["skew"][1].GetDouble();
+ }
+
+ // TODO: fadeDuration
+ // TODO: addFakeCanvas
+
+ return metadata;
+}
+
+// https://stackoverflow.com/questions/7053538/how-do-i-encode-a-string-to-base64-using-only-boost
+std::string encodeBase64(const std::string& data) {
+ using namespace boost::archive::iterators;
+ using base64 = insert_linebreaks<base64_from_binary<transform_width<const char*, 6, 8>>, 72>;
+
+ std::stringstream os;
+ std::copy(base64(data.c_str()), base64(data.c_str() + data.size()), ostream_iterator<char>(os));
+ return os.str();
+}
+
+std::string createResultItem(const TestMetadata& metadata, bool hasFailedTests) {
+ const bool shouldHide = (hasFailedTests && metadata.status == "passed") || (metadata.status.find("ignored") != std::string::npos);
+
+ std::string html;
+ html.append("<div class=\"test " + metadata.status + (shouldHide ? " hide" : "") + "\">\n");
+ html.append(R"(<h2><span class="label" style="background: )" + metadata.color + "\">" + metadata.status + "</span> " + metadata.id + "</h2>\n");
+ if (metadata.status != "errored") {
+ html.append("<img width=" + mbgl::util::toString(metadata.size.width));
+ html.append(" height=" + mbgl::util::toString(metadata.size.height));
+ html.append(" src=\"data:image/png;base64," + encodeBase64(metadata.actual) + "\"");
+ html.append(" data-alt-src=\"data:image/png;base64," + encodeBase64(metadata.expected) + "\">\n");
+
+ html.append("<img width=" + mbgl::util::toString(metadata.size.width));
+ html.append(" height=" + mbgl::util::toString(metadata.size.height));
+ html.append(" src=\"data:image/png;base64," + encodeBase64(metadata.diff) + "\">\n");
+ } else {
+ assert(!metadata.errorMessage.empty());
+ html.append("<p style=\"color: red\"><strong>Error:</strong> " + metadata.errorMessage + "</p>\n");
+ }
+ if (metadata.difference != 0.0) {
+ html.append("<p class=\"diff\"><strong>Diff:</strong> " + mbgl::util::toString(metadata.difference) + "</p>\n");
+ }
+ html.append("</div>\n");
+
+ return html;
+}
+
+std::string createResultPage(const TestStatistics& stats, const std::vector<TestMetadata>& metadatas, bool shuffle, uint32_t seed) {
+ const uint32_t unsuccessful = stats.erroredTests + stats.failedTests;
+ std::string resultsPage;
+
+ // Style
+ resultsPage.append(resultsStyle);
+
+ // Header
+ if (unsuccessful) {
+ resultsPage.append(R"HTML(<h1 style="color: red;">)HTML");
+ resultsPage.append(mbgl::util::toString(unsuccessful) + " tests failed.");
+ } else {
+ resultsPage.append(R"HTML(<h1 style="color: green;">)HTML");
+ resultsPage.append("All tests passed!");
+ }
+ resultsPage.append(resultsHeaderButtons);
+
+ // stats
+ resultsPage.append(R"HTML(<p class="stats">)HTML");
+ if (stats.ignoreFailedTests) {
+ resultsPage.append(mbgl::util::toString(stats.ignoreFailedTests) + " ignored failed, ");
+ }
+ if (stats.ignorePassedTests) {
+ resultsPage.append(mbgl::util::toString(stats.ignorePassedTests) + " ignored passed, ");
+ }
+ if (stats.erroredTests) {
+ resultsPage.append(mbgl::util::toString(stats.erroredTests) + " errored, ");
+ }
+ if (stats.failedTests) {
+ resultsPage.append(mbgl::util::toString(stats.failedTests) + " failed, ");
+ }
+ resultsPage.append(mbgl::util::toString(stats.passedTests) + " passed.\n");
+ resultsPage.append("</p>\n");
+
+ // Test sequence
+ {
+ resultsPage.append("<div id='test-sequence' class='hide'>\n");
+
+ // Failed tests
+ if (unsuccessful) {
+ resultsPage.append("<p><strong>Failed tests:</strong>");
+ for (const auto& metadata : metadatas) {
+ if (metadata.status == "failed" || metadata.status == "errored") {
+ resultsPage.append(metadata.id + " ");
+ }
+ }
+ resultsPage.append("</p>\n");
+ }
+
+ // Test sequence
+ resultsPage.append("<p><strong>Test sequence:</strong>");
+ for (const auto& metadata : metadatas) {
+ resultsPage.append(metadata.id + " ");
+ }
+ resultsPage.append("</p>\n");
+
+ // Shuffle
+ if (shuffle) {
+ resultsPage.append("<p><strong>Shuffle seed</strong>: " + mbgl::util::toString(seed) + "</p>\n");
+ }
+
+ resultsPage.append("</div>\n");
+ }
+
+ // Script
+ resultsPage.append(resultsScript);
+
+ // Tests
+ resultsPage.append("<div class=\"tests\">\n");
+ for (const auto& metadata : metadatas) {
+ resultsPage.append(createResultItem(metadata, unsuccessful));
+ }
+ resultsPage.append("</div>\n");
+
+ return resultsPage;
+}
+
+std::string localizeURL(const std::string& url) {
+ static const std::regex regex { "local://" };
+ if (auto vendorPath = getVendorPath(url, regex)) {
+ return *vendorPath;
+ } else {
+ return getIntegrationPath(url, "", regex).value_or(url);
+ }
+}
+
+void localizeSourceURLs(mbgl::JSValue& root, mbgl::JSDocument& document) {
+ if (root.HasMember("urls") && root["urls"].IsArray()) {
+ for (auto& urlValue : root["urls"].GetArray()) {
+ const std::string path = prependFileScheme(localizeMapboxTilesetURL(urlValue.GetString())
+ .value_or(localizeLocalURL(urlValue.GetString())
+ .value_or(urlValue.GetString())));
+ urlValue.Set<std::string>(path, document.GetAllocator());
+ }
+ }
+
+ if (root.HasMember("url")) {
+ static const std::string image("image");
+ static const std::string video("video");
+
+ mbgl::JSValue& urlValue = root["url"];
+ const std::string path = prependFileScheme(localizeMapboxTilesetURL(urlValue.GetString())
+ .value_or(localizeLocalURL(urlValue.GetString())
+ .value_or(urlValue.GetString())));
+ urlValue.Set<std::string>(path, document.GetAllocator());
+
+ if (root["type"].GetString() != image && root["type"].GetString() != video) {
+ const auto tilesetPath = std::string(urlValue.GetString()).erase(0u, 7u); // remove "file://"
+ auto maybeTileset = readJson(tilesetPath);
+ if (maybeTileset.is<mbgl::JSDocument>()) {
+ const auto& tileset = maybeTileset.get<mbgl::JSDocument>();
+ assert(tileset.HasMember("tiles"));
+ root.AddMember("tiles", (mbgl::JSValue&)tileset["tiles"], document.GetAllocator());
+ root.RemoveMember("url");
+ }
+ }
+ }
+
+ if (root.HasMember("tiles")) {
+ mbgl::JSValue& tilesValue = root["tiles"];
+ assert(tilesValue.IsArray());
+ for (auto& tileValue : tilesValue.GetArray()) {
+ const std::string path = prependFileScheme(localizeMapboxTilesURL(tileValue.GetString())
+ .value_or(localizeLocalURL(tileValue.GetString())
+ .value_or(localizeHttpURL(tileValue.GetString())
+ .value_or(tileValue.GetString()))));
+ tileValue.Set<std::string>(path, document.GetAllocator());
+ }
+ }
+
+ if (root.HasMember("data") && root["data"].IsString()) {
+ mbgl::JSValue& dataValue = root["data"];
+ const std::string path = prependFileScheme(localizeLocalURL(dataValue.GetString())
+ .value_or(dataValue.GetString()));
+ dataValue.Set<std::string>(path, document.GetAllocator());
+ }
+}
+
+void localizeStyleURLs(mbgl::JSValue& root, mbgl::JSDocument& document) {
+ if (root.HasMember("sources")) {
+ mbgl::JSValue& sourcesValue = root["sources"];
+ for (auto& sourceProperty : sourcesValue.GetObject()) {
+ localizeSourceURLs(sourceProperty.value, document);
+ }
+ }
+
+ if (root.HasMember("glyphs")) {
+ mbgl::JSValue& glyphsValue = root["glyphs"];
+ const std::string path = prependFileScheme(localizeMapboxFontsURL(glyphsValue.GetString())
+ .value_or(localizeLocalURL(glyphsValue.GetString(), true)
+ .value_or(glyphsValue.GetString())));
+ glyphsValue.Set<std::string>(path, document.GetAllocator());
+ }
+
+ if (root.HasMember("sprite")) {
+ mbgl::JSValue& spriteValue = root["sprite"];
+ const std::string path = prependFileScheme(localizeMapboxSpriteURL(spriteValue.GetString())
+ .value_or(localizeLocalURL(spriteValue.GetString())
+ .value_or(spriteValue.GetString())));
+ spriteValue.Set<std::string>(path, document.GetAllocator());
+ }
+}
diff --git a/render-test/parser.hpp b/render-test/parser.hpp
new file mode 100644
index 0000000000..be98719ab5
--- /dev/null
+++ b/render-test/parser.hpp
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <mbgl/util/rapidjson.hpp>
+#include <mbgl/util/variant.hpp>
+
+#include <tuple>
+#include <string>
+#include <vector>
+
+#include "filesystem.hpp"
+
+struct TestMetadata;
+struct TestStatistics;
+
+using ErrorMessage = std::string;
+using JSONReply = mbgl::variant<mbgl::JSDocument, ErrorMessage>;
+
+using ArgumentsTuple = std::tuple<bool, bool, uint32_t, std::string, std::vector<std::string>>;
+
+JSONReply readJson(const mbgl::filesystem::path&);
+std::string serializeJsonValue(const mbgl::JSValue&);
+
+std::vector<std::string> readExpectedEntries(const mbgl::filesystem::path& base);
+
+ArgumentsTuple parseArguments(int argc, char** argv);
+std::vector<std::pair<std::string, std::string>> parseIgnores();
+TestMetadata parseTestMetadata(const mbgl::filesystem::path& path);
+
+std::string createResultPage(const TestStatistics&, const std::vector<TestMetadata>&, bool shuffle, uint32_t seed);
+
+std::string localizeURL(const std::string& url);
+
+void localizeSourceURLs(mbgl::JSValue& root, mbgl::JSDocument& document);
+void localizeStyleURLs(mbgl::JSValue& root, mbgl::JSDocument& document); \ No newline at end of file
diff --git a/render-test/runner.cpp b/render-test/runner.cpp
new file mode 100644
index 0000000000..dd6aef3a54
--- /dev/null
+++ b/render-test/runner.cpp
@@ -0,0 +1,418 @@
+#include <mbgl/map/camera.hpp>
+#include <mbgl/map/map_observer.hpp>
+#include <mbgl/style/conversion/filter.hpp>
+#include <mbgl/style/conversion/layer.hpp>
+#include <mbgl/style/conversion/light.hpp>
+#include <mbgl/style/conversion/source.hpp>
+#include <mbgl/style/image.hpp>
+#include <mbgl/style/layer.hpp>
+#include <mbgl/style/light.hpp>
+#include <mbgl/style/style.hpp>
+#include <mbgl/style/rapidjson_conversion.hpp>
+#include <mbgl/util/chrono.hpp>
+#include <mbgl/util/io.hpp>
+#include <mbgl/util/image.hpp>
+#include <mbgl/util/run_loop.hpp>
+#include <mbgl/util/string.hpp>
+#include <mbgl/util/timer.hpp>
+
+#include <mapbox/pixelmatch.hpp>
+
+#include "metadata.hpp"
+#include "parser.hpp"
+#include "runner.hpp"
+
+#include <algorithm>
+#include <cassert>
+#include <regex>
+
+bool TestRunner::checkImage(mbgl::PremultipliedImage&& actual, TestMetadata& metadata) {
+ const std::string base = metadata.path.remove_filename().string();
+ metadata.actual = mbgl::encodePNG(actual);
+
+ if (actual.size.isEmpty()) {
+ metadata.errorMessage = "Invalid size for actual image";
+ return false;
+ }
+
+#if !TEST_READ_ONLY
+ if (getenv("UPDATE")) {
+ mbgl::util::write_file(base + "/expected.png", mbgl::encodePNG(actual));
+ return true;
+ }
+
+ mbgl::util::write_file(base + "/actual.png", metadata.actual);
+#endif
+
+ mbgl::PremultipliedImage expected { actual.size };
+ mbgl::PremultipliedImage diff { actual.size };
+
+ double pixels = 0.0;
+
+ for (const auto& entry: readExpectedEntries(base)) {
+ mbgl::optional<std::string> maybeExpectedImage = mbgl::util::readFile(entry);
+ if (!maybeExpectedImage) {
+ metadata.errorMessage = "Failed to load expected image " + entry;
+ return false;
+ }
+
+ metadata.expected = *maybeExpectedImage;
+
+ expected = mbgl::decodeImage(*maybeExpectedImage);
+
+ pixels = // implicitly converting from uint64_t
+ mapbox::pixelmatch(actual.data.get(), expected.data.get(), expected.size.width,
+ expected.size.height, diff.data.get(), 0.16); // GL JS uses 0.1285
+
+ metadata.diff = mbgl::encodePNG(diff);
+
+#if !TEST_READ_ONLY
+ mbgl::util::write_file(base + "/diff.png", metadata.diff);
+#endif
+
+ metadata.difference = pixels / expected.size.area();
+ if (metadata.difference <= metadata.allowed) {
+ break;
+ }
+ }
+
+ return true;
+}
+
+bool TestRunner::runOperations(const std::string& key, TestMetadata& metadata) {
+ if (!metadata.document.HasMember("metadata") ||
+ !metadata.document["metadata"].HasMember("test") ||
+ !metadata.document["metadata"]["test"].HasMember("operations")) {
+ return true;
+ }
+
+ assert(metadata.document["metadata"]["test"]["operations"].IsArray());
+
+ const auto& operationsArray = metadata.document["metadata"]["test"]["operations"].GetArray();
+ if (operationsArray.Empty()) {
+ return true;
+ }
+
+ const auto& operationIt = operationsArray.Begin();
+ assert(operationIt->IsArray());
+
+ const auto& operationArray = operationIt->GetArray();
+ assert(operationArray.Size() >= 1u);
+
+ auto& frontend = maps[key]->frontend;
+ auto& map = maps[key]->map;
+
+ static const std::string waitOp("wait");
+ static const std::string sleepOp("sleep");
+ static const std::string addImageOp("addImage");
+ static const std::string updateImageOp("updateImage");
+ static const std::string removeImageOp("removeImage");
+ static const std::string setStyleOp("setStyle");
+ static const std::string setCenterOp("setCenter");
+ static const std::string setZoomOp("setZoom");
+ static const std::string setBearingOp("setBearing");
+ static const std::string setFilterOp("setFilter");
+ static const std::string setLayerZoomRangeOp("setLayerZoomRange");
+ static const std::string setLightOp("setLight");
+ static const std::string addLayerOp("addLayer");
+ static const std::string removeLayerOp("removeLayer");
+ static const std::string addSourceOp("addSource");
+ static const std::string removeSourceOp("removeSource");
+ static const std::string setPaintPropertyOp("setPaintProperty");
+ static const std::string setLayoutPropertyOp("setLayoutProperty");
+
+ // wait
+ if (operationArray[0].GetString() == waitOp) {
+ try {
+ frontend.render(map);
+ } catch (const std::exception&) {
+ return false;
+ }
+ // sleep
+ } else if (operationArray[0].GetString() == sleepOp) {
+ mbgl::util::Timer sleepTimer;
+ bool sleeping = true;
+
+ mbgl::Duration duration = mbgl::Seconds(3);
+ if (operationArray.Size() >= 2u) {
+ duration = mbgl::Milliseconds(operationArray[1].GetUint());
+ }
+
+ sleepTimer.start(duration, mbgl::Duration::zero(), [&]() {
+ sleeping = false;
+ });
+
+ while (sleeping) {
+ mbgl::util::RunLoop::Get()->runOnce();
+ }
+
+ // addImage | updateImage
+ } else if (operationArray[0].GetString() == addImageOp || operationArray[0].GetString() == updateImageOp) {
+ assert(operationArray.Size() >= 3u);
+
+ float pixelRatio = 1.0f;
+ bool sdf = false;
+
+ if (operationArray.Size() == 4u) {
+ assert(operationArray[3].IsObject());
+ const auto& imageOptions = operationArray[3].GetObject();
+ if (imageOptions.HasMember("pixelRatio")) {
+ pixelRatio = imageOptions["pixelRatio"].GetFloat();
+ }
+ if (imageOptions.HasMember("sdf")) {
+ sdf = imageOptions["sdf"].GetBool();
+ }
+ }
+
+ std::string imageName = operationArray[1].GetString();
+ imageName.erase(std::remove(imageName.begin(), imageName.end(), '"'), imageName.end());
+
+ std::string imagePath = operationArray[2].GetString();
+ imagePath.erase(std::remove(imagePath.begin(), imagePath.end(), '"'), imagePath.end());
+
+ const mbgl::filesystem::path filePath(std::string(TEST_RUNNER_ROOT_PATH) + "/mapbox-gl-js/test/integration/" + imagePath);
+
+ mbgl::optional<std::string> maybeImage = mbgl::util::readFile(filePath.string());
+ if (!maybeImage) {
+ metadata.errorMessage = std::string("Failed to load expected image ") + filePath.string();
+ return false;
+ }
+
+ map.getStyle().addImage(std::make_unique<mbgl::style::Image>(imageName, mbgl::decodeImage(*maybeImage), pixelRatio, sdf));
+
+ // removeImage
+ } else if (operationArray[0].GetString() == removeImageOp) {
+ assert(operationArray.Size() >= 2u);
+ assert(operationArray[1].IsString());
+
+ const std::string imageName { operationArray[1].GetString(), operationArray[1].GetStringLength() };
+ map.getStyle().removeImage(imageName);
+
+ // setStyle
+ } else if (operationArray[0].GetString() == setStyleOp) {
+ assert(operationArray.Size() >= 2u);
+ if (operationArray[1].IsString()) {
+ std::string stylePath = localizeURL(operationArray[1].GetString());
+ auto maybeStyle = readJson(stylePath);
+ if (maybeStyle.is<mbgl::JSDocument>()) {
+ auto& style = maybeStyle.get<mbgl::JSDocument>();
+ localizeStyleURLs((mbgl::JSValue&)style, style);
+ map.getStyle().loadJSON(serializeJsonValue(style));
+ }
+ } else {
+ localizeStyleURLs(operationArray[1], metadata.document);
+ map.getStyle().loadJSON(serializeJsonValue(operationArray[1]));
+ }
+
+ // setCenter
+ } else if (operationArray[0].GetString() == setCenterOp) {
+ assert(operationArray.Size() >= 2u);
+ assert(operationArray[1].IsArray());
+
+ const auto& centerArray = operationArray[1].GetArray();
+ assert(centerArray.Size() == 2u);
+
+ map.jumpTo(mbgl::CameraOptions().withCenter(mbgl::LatLng(centerArray[1].GetDouble(), centerArray[0].GetDouble())));
+
+ // setZoom
+ } else if (operationArray[0].GetString() == setZoomOp) {
+ assert(operationArray.Size() >= 2u);
+ assert(operationArray[1].IsNumber());
+ map.jumpTo(mbgl::CameraOptions().withZoom(operationArray[1].GetDouble()));
+
+ // setBearing
+ } else if (operationArray[0].GetString() == setBearingOp) {
+ assert(operationArray.Size() >= 2u);
+ assert(operationArray[1].IsNumber());
+ map.jumpTo(mbgl::CameraOptions().withBearing(operationArray[1].GetDouble()));
+
+ // setFilter
+ } else if (operationArray[0].GetString() == setFilterOp) {
+ assert(operationArray.Size() >= 3u);
+ assert(operationArray[1].IsString());
+
+ const std::string layerName { operationArray[1].GetString(), operationArray[1].GetStringLength() };
+
+ mbgl::style::conversion::Error error;
+ auto converted = mbgl::style::conversion::convert<mbgl::style::Filter>(operationArray[2], error);
+ if (!converted) {
+ metadata.errorMessage = std::string("Unable to convert filter: ") + error.message;
+ return false;
+ } else {
+ auto layer = map.getStyle().getLayer(layerName);
+ if (!layer) {
+ metadata.errorMessage = std::string("Layer not found: ") + layerName;
+ return false;
+ } else {
+ layer->setFilter(std::move(*converted));
+ }
+ }
+
+ // setLayerZoomRange
+ } else if (operationArray[0].GetString() == setLayerZoomRangeOp) {
+ assert(operationArray.Size() >= 4u);
+ assert(operationArray[1].IsString());
+ assert(operationArray[2].IsNumber());
+ assert(operationArray[3].IsNumber());
+
+ const std::string layerName { operationArray[1].GetString(), operationArray[1].GetStringLength() };
+ auto layer = map.getStyle().getLayer(layerName);
+ if (!layer) {
+ metadata.errorMessage = std::string("Layer not found: ") + layerName;
+ return false;
+ } else {
+ layer->setMinZoom(operationArray[2].GetFloat());
+ layer->setMaxZoom(operationArray[3].GetFloat());
+ }
+
+ // setLight
+ } else if (operationArray[0].GetString() == setLightOp) {
+ assert(operationArray.Size() >= 2u);
+ assert(operationArray[1].IsObject());
+
+ mbgl::style::conversion::Error error;
+ auto converted = mbgl::style::conversion::convert<mbgl::style::Light>(operationArray[1], error);
+ if (!converted) {
+ metadata.errorMessage = std::string("Unable to convert light: ") + error.message;
+ return false;
+ } else {
+ map.getStyle().setLight(std::make_unique<mbgl::style::Light>(std::move(*converted)));
+ }
+
+ // addLayer
+ } else if (operationArray[0].GetString() == addLayerOp) {
+ assert(operationArray.Size() >= 2u);
+ assert(operationArray[1].IsObject());
+
+ mbgl::style::conversion::Error error;
+ auto converted = mbgl::style::conversion::convert<std::unique_ptr<mbgl::style::Layer>>(operationArray[1], error);
+ if (!converted) {
+ metadata.errorMessage = std::string("Unable to convert layer: ") + error.message;
+ return false;
+ } else {
+ map.getStyle().addLayer(std::move(*converted));
+ }
+
+ // removeLayer
+ } else if (operationArray[0].GetString() == removeLayerOp) {
+ assert(operationArray.Size() >= 2u);
+ assert(operationArray[1].IsString());
+ map.getStyle().removeLayer(operationArray[1].GetString());
+
+ // addSource
+ } else if (operationArray[0].GetString() == addSourceOp) {
+ assert(operationArray.Size() >= 3u);
+ assert(operationArray[1].IsString());
+ assert(operationArray[2].IsObject());
+
+ localizeSourceURLs(operationArray[2], metadata.document);
+
+ mbgl::style::conversion::Error error;
+ auto converted = mbgl::style::conversion::convert<std::unique_ptr<mbgl::style::Source>>(operationArray[2], error, operationArray[1].GetString());
+ if (!converted) {
+ metadata.errorMessage = std::string("Unable to convert source: ") + error.message;
+ return false;
+ } else {
+ map.getStyle().addSource(std::move(*converted));
+ }
+
+ // removeSource
+ } else if (operationArray[0].GetString() == removeSourceOp) {
+ assert(operationArray.Size() >= 2u);
+ assert(operationArray[1].IsString());
+ map.getStyle().removeSource(operationArray[1].GetString());
+
+ // setPaintProperty
+ } else if (operationArray[0].GetString() == setPaintPropertyOp) {
+ assert(operationArray.Size() >= 4u);
+ assert(operationArray[1].IsString());
+ assert(operationArray[2].IsString());
+
+ const std::string layerName { operationArray[1].GetString(), operationArray[1].GetStringLength() };
+ const std::string propertyName { operationArray[2].GetString(), operationArray[2].GetStringLength() };
+
+ auto layer = map.getStyle().getLayer(layerName);
+ if (!layer) {
+ metadata.errorMessage = std::string("Layer not found: ") + layerName;
+ return false;
+ } else {
+ const mbgl::JSValue* propertyValue = &operationArray[3];
+ layer->setPaintProperty(propertyName, propertyValue);
+ }
+
+ // setLayoutProperty
+ } else if (operationArray[0].GetString() == setLayoutPropertyOp) {
+ assert(operationArray.Size() >= 4u);
+ assert(operationArray[1].IsString());
+ assert(operationArray[2].IsString());
+
+ const std::string layerName { operationArray[1].GetString(), operationArray[1].GetStringLength() };
+ const std::string propertyName { operationArray[2].GetString(), operationArray[2].GetStringLength() };
+
+ auto layer = map.getStyle().getLayer(layerName);
+ if (!layer) {
+ metadata.errorMessage = std::string("Layer not found: ") + layerName;
+ return false;
+ } else {
+ const mbgl::JSValue* propertyValue = &operationArray[3];
+ layer->setLayoutProperty(propertyName, propertyValue);
+ }
+
+ } else {
+ metadata.errorMessage = std::string("Unsupported operation: ") + operationArray[0].GetString();
+ return false;
+ }
+
+ operationsArray.Erase(operationIt);
+ return runOperations(key, metadata);
+}
+
+TestRunner::Impl::Impl(const TestMetadata& metadata)
+ : frontend(metadata.size, metadata.pixelRatio),
+ map(frontend,
+ mbgl::MapObserver::nullObserver(),
+ mbgl::MapOptions()
+ .withMapMode(metadata.mapMode)
+ .withSize(metadata.size)
+ .withPixelRatio(metadata.pixelRatio)
+ .withCrossSourceCollisions(metadata.crossSourceCollisions),
+ mbgl::ResourceOptions().withCacheOnlyRequestsSupport(false)) {}
+
+bool TestRunner::run(TestMetadata& metadata) {
+ std::string key = mbgl::util::toString(uint32_t(metadata.mapMode))
+ + "/" + mbgl::util::toString(metadata.pixelRatio)
+ + "/" + mbgl::util::toString(uint32_t(metadata.crossSourceCollisions));
+
+ if (maps.find(key) == maps.end()) {
+ maps[key] = std::make_unique<TestRunner::Impl>(metadata);
+ }
+
+ auto& frontend = maps[key]->frontend;
+ auto& map = maps[key]->map;
+
+ frontend.setSize(metadata.size);
+ map.setSize(metadata.size);
+
+ map.setProjectionMode(mbgl::ProjectionMode().withAxonometric(metadata.axonometric).withXSkew(metadata.xSkew).withYSkew(metadata.ySkew));
+ map.setDebug(metadata.debug);
+
+ map.getStyle().loadJSON(serializeJsonValue(metadata.document));
+ map.jumpTo(map.getStyle().getDefaultCamera());
+
+ if (!runOperations(key, metadata)) {
+ return false;
+ }
+
+ mbgl::PremultipliedImage image;
+ try {
+ image = frontend.render(map);
+ } catch (const std::exception&) {
+ return false;
+ }
+
+ return checkImage(std::move(image), metadata);
+}
+
+void TestRunner::reset() {
+ maps.clear();
+}
diff --git a/render-test/runner.hpp b/render-test/runner.hpp
new file mode 100644
index 0000000000..cbc0f42546
--- /dev/null
+++ b/render-test/runner.hpp
@@ -0,0 +1,28 @@
+#pragma once
+
+#include <mbgl/gfx/headless_frontend.hpp>
+#include <mbgl/map/map.hpp>
+
+#include <memory>
+
+struct TestMetadata;
+
+class TestRunner {
+public:
+ TestRunner() = default;
+
+ bool run(TestMetadata&);
+ void reset();
+
+private:
+ bool runOperations(const std::string& key, TestMetadata&);
+ bool checkImage(mbgl::PremultipliedImage&& image, TestMetadata&);
+
+ struct Impl {
+ Impl(const TestMetadata&);
+
+ mbgl::HeadlessFrontend frontend;
+ mbgl::Map map;
+ };
+ std::unordered_map<std::string, std::unique_ptr<Impl>> maps;
+}; \ No newline at end of file
diff --git a/scripts/config.xcconfig.in b/scripts/config.xcconfig.in
index 69ca2424a1..9370ed0948 100644
--- a/scripts/config.xcconfig.in
+++ b/scripts/config.xcconfig.in
@@ -7,3 +7,5 @@ mbgl_core_LINK_LIBRARIES = "$<TARGET_PROPERTY:mbgl-core,XCODE_ATTRIBUTE_XCCONFIG
// mbgl-filesource
mbgl_filesource_INCLUDE_DIRECTORIES = "$<JOIN:$<TARGET_PROPERTY:mbgl-filesource,INTERFACE_INCLUDE_DIRECTORIES>," ">"
mbgl_filesource_LINK_LIBRARIES = "$<TARGET_PROPERTY:mbgl-filesource,XCODE_ATTRIBUTE_XCCONFIG_LINK_LIBRARIES>"
+
+#include? "../../platform/darwin/developer.xcconfig" \ No newline at end of file
diff --git a/scripts/generate-file-lists.js b/scripts/generate-file-lists.js
index f90acbe6e0..c4cf11a4c5 100755
--- a/scripts/generate-file-lists.js
+++ b/scripts/generate-file-lists.js
@@ -130,6 +130,7 @@ generateFileList('vendor/cheap-ruler-cpp-files.json', 'vendor/cheap-ruler-cpp',
generateFileList('vendor/earcut.hpp-files.json', 'vendor/earcut.hpp', vendorRegex, [ "include/**/*.hpp" ]);
generateFileList('vendor/eternal-files.json', 'vendor/eternal', vendorRegex, [ "include/**/*.hpp" ]);
generateFileList('vendor/expected-files.json', 'vendor/expected', vendorRegex, [ "include/expected.hpp" ]);
+generateFileList('vendor/filesystem-files.json', 'vendor/filesystem', vendorRegex, [ "include/**/*.hpp" ]);
generateFileList('vendor/geojson-vt-cpp-files.json', 'vendor/geojson-vt-cpp', vendorRegex, [ "include/**/*.hpp" ]);
generateFileList('vendor/geojson.hpp-files.json', 'vendor/geojson.hpp', vendorRegex, [ "include/**/*.hpp" ]);
generateFileList('vendor/geometry.hpp-files.json', 'vendor/geometry.hpp', vendorRegex, [ "include/**/*.hpp" ]);
diff --git a/src/core-files.json b/src/core-files.json
index ec5836c132..fcc0dba9f5 100644
--- a/src/core-files.json
+++ b/src/core-files.json
@@ -127,6 +127,7 @@
"src/mbgl/renderer/pattern_atlas.cpp",
"src/mbgl/renderer/render_layer.cpp",
"src/mbgl/renderer/render_light.cpp",
+ "src/mbgl/renderer/render_orchestrator.cpp",
"src/mbgl/renderer/render_source.cpp",
"src/mbgl/renderer/render_static_data.cpp",
"src/mbgl/renderer/render_tile.cpp",
@@ -191,6 +192,7 @@
"src/mbgl/style/expression/let.cpp",
"src/mbgl/style/expression/literal.cpp",
"src/mbgl/style/expression/match.cpp",
+ "src/mbgl/style/expression/number_format.cpp",
"src/mbgl/style/expression/parsing_context.cpp",
"src/mbgl/style/expression/step.cpp",
"src/mbgl/style/expression/util.cpp",
@@ -415,6 +417,7 @@
"mbgl/style/expression/let.hpp": "include/mbgl/style/expression/let.hpp",
"mbgl/style/expression/literal.hpp": "include/mbgl/style/expression/literal.hpp",
"mbgl/style/expression/match.hpp": "include/mbgl/style/expression/match.hpp",
+ "mbgl/style/expression/number_format.hpp": "include/mbgl/style/expression/number_format.hpp",
"mbgl/style/expression/parsing_context.hpp": "include/mbgl/style/expression/parsing_context.hpp",
"mbgl/style/expression/step.hpp": "include/mbgl/style/expression/step.hpp",
"mbgl/style/expression/type.hpp": "include/mbgl/style/expression/type.hpp",
@@ -643,11 +646,13 @@
"mbgl/renderer/property_evaluator.hpp": "src/mbgl/renderer/property_evaluator.hpp",
"mbgl/renderer/render_layer.hpp": "src/mbgl/renderer/render_layer.hpp",
"mbgl/renderer/render_light.hpp": "src/mbgl/renderer/render_light.hpp",
+ "mbgl/renderer/render_orchestrator.hpp": "src/mbgl/renderer/render_orchestrator.hpp",
"mbgl/renderer/render_pass.hpp": "src/mbgl/renderer/render_pass.hpp",
"mbgl/renderer/render_source.hpp": "src/mbgl/renderer/render_source.hpp",
"mbgl/renderer/render_source_observer.hpp": "src/mbgl/renderer/render_source_observer.hpp",
"mbgl/renderer/render_static_data.hpp": "src/mbgl/renderer/render_static_data.hpp",
"mbgl/renderer/render_tile.hpp": "src/mbgl/renderer/render_tile.hpp",
+ "mbgl/renderer/render_tree.hpp": "src/mbgl/renderer/render_tree.hpp",
"mbgl/renderer/renderer_impl.hpp": "src/mbgl/renderer/renderer_impl.hpp",
"mbgl/renderer/sources/render_custom_geometry_source.hpp": "src/mbgl/renderer/sources/render_custom_geometry_source.hpp",
"mbgl/renderer/sources/render_geojson_source.hpp": "src/mbgl/renderer/sources/render_geojson_source.hpp",
diff --git a/src/mbgl/renderer/layers/render_heatmap_layer.cpp b/src/mbgl/renderer/layers/render_heatmap_layer.cpp
index ba6e6b7a26..4b80dee5e7 100644
--- a/src/mbgl/renderer/layers/render_heatmap_layer.cpp
+++ b/src/mbgl/renderer/layers/render_heatmap_layer.cpp
@@ -41,7 +41,7 @@ void RenderHeatmapLayer::evaluate(const PropertyEvaluationParameters& parameters
unevaluated.evaluate(parameters));
passes = (properties->evaluated.get<style::HeatmapOpacity>() > 0)
- ? (RenderPass::Translucent | RenderPass::Pass3D | RenderPass::Upload)
+ ? (RenderPass::Translucent | RenderPass::Pass3D)
: RenderPass::None;
evaluatedProperties = std::move(properties);
@@ -55,7 +55,7 @@ bool RenderHeatmapLayer::hasCrossfade() const {
return false;
}
-void RenderHeatmapLayer::upload(gfx::UploadPass& uploadPass, UploadParameters&) {
+void RenderHeatmapLayer::upload(gfx::UploadPass& uploadPass) {
if (!colorRampTexture) {
colorRampTexture =
uploadPass.createTexture(colorRamp, gfx::TextureChannelDataType::UnsignedByte);
diff --git a/src/mbgl/renderer/layers/render_heatmap_layer.hpp b/src/mbgl/renderer/layers/render_heatmap_layer.hpp
index f331ef8435..27e27adb28 100644
--- a/src/mbgl/renderer/layers/render_heatmap_layer.hpp
+++ b/src/mbgl/renderer/layers/render_heatmap_layer.hpp
@@ -19,7 +19,7 @@ private:
void evaluate(const PropertyEvaluationParameters&) override;
bool hasTransition() const override;
bool hasCrossfade() const override;
- void upload(gfx::UploadPass&, UploadParameters&) override;
+ void upload(gfx::UploadPass&) override;
void render(PaintParameters&) override;
bool queryIntersectsFeature(
diff --git a/src/mbgl/renderer/layers/render_line_layer.cpp b/src/mbgl/renderer/layers/render_line_layer.cpp
index 5b20e4667c..595140634d 100644
--- a/src/mbgl/renderer/layers/render_line_layer.cpp
+++ b/src/mbgl/renderer/layers/render_line_layer.cpp
@@ -47,7 +47,7 @@ void RenderLineLayer::evaluate(const PropertyEvaluationParameters& parameters) {
passes = (evaluated.get<style::LineOpacity>().constantOr(1.0) > 0
&& evaluated.get<style::LineColor>().constantOr(Color::black()).a > 0
&& evaluated.get<style::LineWidth>().constantOr(1.0) > 0)
- ? RenderPass::Translucent | RenderPass::Upload : RenderPass::None;
+ ? RenderPass::Translucent : RenderPass::None;
evaluatedProperties = std::move(properties);
}
@@ -59,34 +59,27 @@ bool RenderLineLayer::hasCrossfade() const {
return getCrossfade<LineLayerProperties>(evaluatedProperties).t != 1;
}
-void RenderLineLayer::upload(gfx::UploadPass& uploadPass, UploadParameters& uploadParameters) {
+void RenderLineLayer::prepare(const LayerPrepareParameters& params) {
+ RenderLayer::prepare(params);
for (const RenderTile& tile : renderTiles) {
const LayerRenderData* renderData = tile.getLayerRenderData(*baseImpl);
- if (!renderData) {
- continue;
- }
- auto& bucket = static_cast<LineBucket&>(*renderData->bucket);
- const auto& evaluated = getEvaluated<LineLayerProperties>(renderData->layerProperties);
-
- if (!evaluated.get<LineDasharray>().from.empty()) {
- const LinePatternCap cap = bucket.layout.get<LineCap>() == LineCapType::Round
- ? LinePatternCap::Round : LinePatternCap::Square;
- // Ensures that the dash data gets added and uploaded to the atlas.
- uploadParameters.lineAtlas.getDashPosition(evaluated.get<LineDasharray>().from, cap);
- uploadParameters.lineAtlas.getDashPosition(evaluated.get<LineDasharray>().to, cap);
+ if (!renderData) continue;
- } else if (!unevaluated.get<LinePattern>().isUndefined()) {
- const auto& linePatternValue = evaluated.get<LinePattern>().constantOr(Faded<std::basic_string<char>>{ "", ""});
+ const auto& evaluated = getEvaluated<LineLayerProperties>(renderData->layerProperties);
+ if (evaluated.get<LineDasharray>().from.empty()) continue;
- // Ensures that the pattern gets added and uplodated to the atlas.
- tile.getPattern(linePatternValue.from);
- tile.getPattern(linePatternValue.to);
+ auto& bucket = static_cast<LineBucket&>(*renderData->bucket);
+ const LinePatternCap cap = bucket.layout.get<LineCap>() == LineCapType::Round
+ ? LinePatternCap::Round : LinePatternCap::Square;
+ // Ensures that the dash data gets added to the atlas.
+ params.lineAtlas.getDashPosition(evaluated.get<LineDasharray>().from, cap);
+ params.lineAtlas.getDashPosition(evaluated.get<LineDasharray>().to, cap);
+ }
+}
- } else if (!unevaluated.get<LineGradient>().getValue().isUndefined()) {
- if (!colorRampTexture) {
- colorRampTexture = uploadPass.createTexture(colorRamp);
- }
- }
+void RenderLineLayer::upload(gfx::UploadPass& uploadPass) {
+ if (!unevaluated.get<LineGradient>().getValue().isUndefined() && !colorRampTexture) {
+ colorRampTexture = uploadPass.createTexture(colorRamp);
}
}
diff --git a/src/mbgl/renderer/layers/render_line_layer.hpp b/src/mbgl/renderer/layers/render_line_layer.hpp
index f57686e1c8..4454d215d9 100644
--- a/src/mbgl/renderer/layers/render_line_layer.hpp
+++ b/src/mbgl/renderer/layers/render_line_layer.hpp
@@ -20,7 +20,8 @@ private:
void evaluate(const PropertyEvaluationParameters&) override;
bool hasTransition() const override;
bool hasCrossfade() const override;
- void upload(gfx::UploadPass&, UploadParameters&) override;
+ void prepare(const LayerPrepareParameters&) override;
+ void upload(gfx::UploadPass&) override;
void render(PaintParameters&) override;
bool queryIntersectsFeature(
diff --git a/src/mbgl/renderer/layers/render_symbol_layer.cpp b/src/mbgl/renderer/layers/render_symbol_layer.cpp
index 51af6fa817..33b095b7e7 100644
--- a/src/mbgl/renderer/layers/render_symbol_layer.cpp
+++ b/src/mbgl/renderer/layers/render_symbol_layer.cpp
@@ -262,7 +262,7 @@ void RenderSymbolLayer::evaluate(const PropertyEvaluationParameters& parameters)
passes = ((evaluated.get<style::IconOpacity>().constantOr(1) > 0 && hasIconOpacity && iconSize > 0)
|| (evaluated.get<style::TextOpacity>().constantOr(1) > 0 && hasTextOpacity && textSize > 0))
- ? RenderPass::Translucent | RenderPass::Upload : RenderPass::None;
+ ? RenderPass::Translucent : RenderPass::None;
evaluatedProperties = std::move(properties);
}
diff --git a/src/mbgl/renderer/paint_parameters.cpp b/src/mbgl/renderer/paint_parameters.cpp
index badd60a53e..5034850264 100644
--- a/src/mbgl/renderer/paint_parameters.cpp
+++ b/src/mbgl/renderer/paint_parameters.cpp
@@ -28,8 +28,10 @@ TransformParameters::TransformParameters(const TransformState& state_)
PaintParameters::PaintParameters(gfx::Context& context_,
float pixelRatio_,
gfx::RendererBackend& backend_,
- const UpdateParameters& updateParameters,
const EvaluatedLight& evaluatedLight_,
+ MapMode mode_,
+ MapDebugOptions debugOptions_,
+ TimePoint timePoint_,
const TransformParameters& transformParams_,
RenderStaticData& staticData_,
LineAtlas& lineAtlas_,
@@ -37,15 +39,15 @@ PaintParameters::PaintParameters(gfx::Context& context_,
: context(context_),
backend(backend_),
encoder(context.createCommandEncoder()),
- state(updateParameters.transformState),
- evaluatedLight(evaluatedLight_),
transformParams(transformParams_),
+ state(transformParams_.state),
+ evaluatedLight(evaluatedLight_),
staticData(staticData_),
lineAtlas(lineAtlas_),
patternAtlas(patternAtlas_),
- mapMode(updateParameters.mode),
- debugOptions(updateParameters.debugOptions),
- timePoint(updateParameters.timePoint),
+ mapMode(mode_),
+ debugOptions(debugOptions_),
+ timePoint(timePoint_),
pixelRatio(pixelRatio_),
#ifndef NDEBUG
programs((debugOptions & MapDebugOptions::Overdraw) ? staticData_.overdrawPrograms : staticData_.programs)
diff --git a/src/mbgl/renderer/paint_parameters.hpp b/src/mbgl/renderer/paint_parameters.hpp
index 89084e9e52..d78125da54 100644
--- a/src/mbgl/renderer/paint_parameters.hpp
+++ b/src/mbgl/renderer/paint_parameters.hpp
@@ -3,6 +3,7 @@
#include <mbgl/renderer/render_pass.hpp>
#include <mbgl/renderer/render_light.hpp>
#include <mbgl/map/mode.hpp>
+#include <mbgl/map/transform_state.hpp>
#include <mbgl/gfx/depth_mode.hpp>
#include <mbgl/gfx/stencil_mode.hpp>
#include <mbgl/gfx/color_mode.hpp>
@@ -39,7 +40,7 @@ public:
mat4 projMatrix;
mat4 alignedProjMatrix;
mat4 nearClippedProjMatrix;
- const TransformState& state;
+ const TransformState state;
};
class PaintParameters {
@@ -47,8 +48,10 @@ public:
PaintParameters(gfx::Context&,
float pixelRatio,
gfx::RendererBackend&,
- const UpdateParameters&,
const EvaluatedLight&,
+ MapMode,
+ MapDebugOptions,
+ TimePoint,
const TransformParameters&,
RenderStaticData&,
LineAtlas&,
@@ -60,9 +63,9 @@ public:
std::unique_ptr<gfx::CommandEncoder> encoder;
std::unique_ptr<gfx::RenderPass> renderPass;
+ const TransformParameters& transformParams;
const TransformState& state;
const EvaluatedLight& evaluatedLight;
- const TransformParameters& transformParams;
RenderStaticData& staticData;
LineAtlas& lineAtlas;
diff --git a/src/mbgl/renderer/paint_property_binder.hpp b/src/mbgl/renderer/paint_property_binder.hpp
index 63d821f964..cd6b259e88 100644
--- a/src/mbgl/renderer/paint_property_binder.hpp
+++ b/src/mbgl/renderer/paint_property_binder.hpp
@@ -544,18 +544,6 @@ public:
return binders.template get<P>()->statistics;
}
- using Bitset = std::bitset<sizeof...(Ps)>;
-
- template <class EvaluatedProperties>
- static Bitset constants(const EvaluatedProperties& currentProperties) {
- Bitset result;
- util::ignore({
- result.set(TypeIndex<Ps, Ps...>::value,
- currentProperties.template get<Ps>().isConstant())...
- });
- return result;
- }
-
private:
Binders binders;
};
diff --git a/src/mbgl/renderer/render_layer.cpp b/src/mbgl/renderer/render_layer.cpp
index a18862ce0a..4ad93a0d16 100644
--- a/src/mbgl/renderer/render_layer.cpp
+++ b/src/mbgl/renderer/render_layer.cpp
@@ -78,21 +78,21 @@ void RenderLayer::checkRenderability(const PaintParameters& parameters,
}
if (activeBindingCount > parameters.context.maximumVertexBindingCount) {
- Log::Error(Event::OpenGL,
- "The layer '%s' uses more data-driven properties than the current device "
- "supports, and will have rendering errors. To ensure compatibility with this "
- "device, use %d fewer data driven properties in this layer.",
- getID().c_str(),
- activeBindingCount - parameters.context.minimumRequiredVertexBindingCount);
+ Log::Info(Event::OpenGL,
+ "The layer '%s' uses more data-driven properties than the current device "
+ "supports, and will have rendering errors. To ensure compatibility with this "
+ "device, use %d fewer data driven properties in this layer.",
+ getID().c_str(),
+ activeBindingCount - parameters.context.minimumRequiredVertexBindingCount);
hasRenderFailures = true;
} else if (activeBindingCount > parameters.context.minimumRequiredVertexBindingCount) {
- Log::Warning(Event::OpenGL,
- "The layer '%s' uses more data-driven properties than some devices may support. "
- "Though it will render correctly on this device, it may have rendering errors "
- "on other devices. To ensure compatibility with all devices, use %d fewer "
- "data-driven properties in this layer.",
- getID().c_str(),
- activeBindingCount - parameters.context.minimumRequiredVertexBindingCount);
+ Log::Info(Event::OpenGL,
+ "The layer '%s' uses more data-driven properties than some devices may support. "
+ "Though it will render correctly on this device, it may have rendering errors "
+ "on other devices. To ensure compatibility with all devices, use %d fewer "
+ "data-driven properties in this layer.",
+ getID().c_str(),
+ activeBindingCount - parameters.context.minimumRequiredVertexBindingCount);
hasRenderFailures = true;
}
}
diff --git a/src/mbgl/renderer/render_layer.hpp b/src/mbgl/renderer/render_layer.hpp
index 94d7384ba4..1eaa79ab2b 100644
--- a/src/mbgl/renderer/render_layer.hpp
+++ b/src/mbgl/renderer/render_layer.hpp
@@ -19,6 +19,7 @@ class RenderSource;
class RenderTile;
class TransformState;
class PatternAtlas;
+class LineAtlas;
class LayerRenderData {
public:
@@ -37,6 +38,7 @@ public:
RenderSource* source;
ImageManager& imageManager;
PatternAtlas& patternAtlas;
+ LineAtlas& lineAtlas;
const TransformState& state;
};
@@ -77,7 +79,7 @@ public:
// Checks whether the given zoom is inside this layer zoom range.
bool supportsZoom(float zoom) const;
- virtual void upload(gfx::UploadPass&, UploadParameters&) {}
+ virtual void upload(gfx::UploadPass&) {}
virtual void render(PaintParameters&) = 0;
// Check wether the given geometry intersects
diff --git a/src/mbgl/renderer/render_orchestrator.cpp b/src/mbgl/renderer/render_orchestrator.cpp
new file mode 100644
index 0000000000..acc50318d6
--- /dev/null
+++ b/src/mbgl/renderer/render_orchestrator.cpp
@@ -0,0 +1,635 @@
+#include <mbgl/renderer/render_orchestrator.hpp>
+
+#include <mbgl/annotation/annotation_manager.hpp>
+#include <mbgl/layermanager/layer_manager.hpp>
+#include <mbgl/renderer/renderer_observer.hpp>
+#include <mbgl/renderer/render_source.hpp>
+#include <mbgl/renderer/render_layer.hpp>
+#include <mbgl/renderer/render_static_data.hpp>
+#include <mbgl/renderer/render_tree.hpp>
+#include <mbgl/renderer/update_parameters.hpp>
+#include <mbgl/renderer/upload_parameters.hpp>
+#include <mbgl/renderer/pattern_atlas.hpp>
+#include <mbgl/renderer/paint_parameters.hpp>
+#include <mbgl/renderer/transition_parameters.hpp>
+#include <mbgl/renderer/property_evaluation_parameters.hpp>
+#include <mbgl/renderer/tile_parameters.hpp>
+#include <mbgl/renderer/render_tile.hpp>
+#include <mbgl/renderer/style_diff.hpp>
+#include <mbgl/renderer/query.hpp>
+#include <mbgl/renderer/image_manager.hpp>
+#include <mbgl/geometry/line_atlas.hpp>
+#include <mbgl/style/source_impl.hpp>
+#include <mbgl/style/transition_options.hpp>
+#include <mbgl/text/glyph_manager.hpp>
+#include <mbgl/tile/tile.hpp>
+#include <mbgl/util/math.hpp>
+#include <mbgl/util/string.hpp>
+#include <mbgl/util/logging.hpp>
+
+namespace mbgl {
+
+using namespace style;
+
+static RendererObserver& nullObserver() {
+ static RendererObserver observer;
+ return observer;
+}
+
+namespace {
+
+class LayerRenderItem final : public RenderItem {
+public:
+ LayerRenderItem(RenderLayer& layer_, RenderSource* source_, uint32_t index_)
+ : layer(layer_), source(source_), index(index_) {}
+ bool operator<(const LayerRenderItem& other) const { return index < other.index; }
+ std::reference_wrapper<RenderLayer> layer;
+ RenderSource* source;
+
+private:
+ bool hasRenderPass(RenderPass pass) const override { return layer.get().hasRenderPass(pass); }
+ void upload(gfx::UploadPass& pass) const override { layer.get().upload(pass); }
+ void render(PaintParameters& parameters) const override { layer.get().render(parameters); }
+ const std::string& getName() const override { return layer.get().getID(); }
+
+ uint32_t index;
+};
+
+class SourceRenderItem final : public RenderItem {
+public:
+ explicit SourceRenderItem(RenderSource& source_)
+ : source(source_) {}
+
+private:
+ bool hasRenderPass(RenderPass) const override { return false; }
+ void upload(gfx::UploadPass& pass) const override { source.get().upload(pass); }
+ void render(PaintParameters& parameters) const override { source.get().finishRender(parameters); }
+ const std::string& getName() const override { return source.get().baseImpl->id; }
+
+ std::reference_wrapper<RenderSource> source;
+};
+
+class RenderTreeImpl final : public RenderTree {
+public:
+ RenderTreeImpl(std::unique_ptr<RenderTreeParameters> parameters_,
+ std::set<LayerRenderItem> layerRenderItems_,
+ std::vector<SourceRenderItem> sourceRenderItems_,
+ LineAtlas& lineAtlas_,
+ PatternAtlas& patternAtlas_)
+ : RenderTree(std::move(parameters_)),
+ layerRenderItems(std::move(layerRenderItems_)),
+ sourceRenderItems(std::move(sourceRenderItems_)),
+ lineAtlas(lineAtlas_),
+ patternAtlas(patternAtlas_) {
+ }
+
+ RenderItems getLayerRenderItems() const override {
+ return { layerRenderItems.begin(), layerRenderItems.end() };
+ }
+ RenderItems getSourceRenderItems() const override {
+ return { sourceRenderItems.begin(), sourceRenderItems.end() };
+ }
+ LineAtlas& getLineAtlas() const override { return lineAtlas; }
+ PatternAtlas& getPatternAtlas() const override { return patternAtlas; }
+
+ std::set<LayerRenderItem> layerRenderItems;
+ std::vector<SourceRenderItem> sourceRenderItems;
+ std::reference_wrapper<LineAtlas> lineAtlas;
+ std::reference_wrapper<PatternAtlas> patternAtlas;
+};
+
+} // namespace
+
+RenderOrchestrator::RenderOrchestrator(
+ bool backgroundLayerAsColor_,
+ optional<std::string> localFontFamily_)
+ : observer(&nullObserver())
+ , glyphManager(std::make_unique<GlyphManager>(std::make_unique<LocalGlyphRasterizer>(std::move(localFontFamily_))))
+ , imageManager(std::make_unique<ImageManager>())
+ , lineAtlas(std::make_unique<LineAtlas>(Size{ 256, 512 }))
+ , patternAtlas(std::make_unique<PatternAtlas>())
+ , imageImpls(makeMutable<std::vector<Immutable<style::Image::Impl>>>())
+ , sourceImpls(makeMutable<std::vector<Immutable<style::Source::Impl>>>())
+ , layerImpls(makeMutable<std::vector<Immutable<style::Layer::Impl>>>())
+ , renderLight(makeMutable<Light::Impl>())
+ , placement(std::make_unique<Placement>(TransformState{}, MapMode::Static, TransitionOptions{}, true))
+ , backgroundLayerAsColor(backgroundLayerAsColor_) {
+ glyphManager->setObserver(this);
+ imageManager->setObserver(this);
+}
+
+RenderOrchestrator::~RenderOrchestrator() {
+ if (contextLost) {
+ // Signal all RenderLayers that the context was lost
+ // before cleaning up. At the moment, only CustomLayer is
+ // interested whether rendering context is lost. However, it would be
+ // beneficial for dynamically loaded or other custom built-in plugins.
+ for (const auto& entry : renderLayers) {
+ RenderLayer& layer = *entry.second;
+ layer.markContextDestroyed();
+ }
+ }
+};
+
+void RenderOrchestrator::setObserver(RendererObserver* observer_) {
+ observer = observer_ ? observer_ : &nullObserver();
+}
+
+std::unique_ptr<RenderTree> RenderOrchestrator::createRenderTree(const UpdateParameters& updateParameters) {
+ const bool isMapModeContinuous = updateParameters.mode == MapMode::Continuous;
+ if (!isMapModeContinuous) {
+ // Reset zoom history state.
+ zoomHistory.first = true;
+ }
+
+ if (LayerManager::annotationsEnabled) {
+ updateParameters.annotationManager.updateData();
+ }
+
+ const bool zoomChanged = zoomHistory.update(updateParameters.transformState.getZoom(), updateParameters.timePoint);
+
+ const TransitionOptions transitionOptions = isMapModeContinuous ? updateParameters.transitionOptions : TransitionOptions();
+
+ const TransitionParameters transitionParameters {
+ updateParameters.timePoint,
+ transitionOptions
+ };
+
+ const PropertyEvaluationParameters evaluationParameters {
+ zoomHistory,
+ updateParameters.timePoint,
+ transitionOptions.duration.value_or(isMapModeContinuous ? util::DEFAULT_TRANSITION_DURATION : Duration::zero())
+ };
+
+ const TileParameters tileParameters {
+ updateParameters.pixelRatio,
+ updateParameters.debugOptions,
+ updateParameters.transformState,
+ updateParameters.fileSource,
+ updateParameters.mode,
+ updateParameters.annotationManager,
+ *imageManager,
+ *glyphManager,
+ updateParameters.prefetchZoomDelta
+ };
+
+ glyphManager->setURL(updateParameters.glyphURL);
+
+ // Update light.
+ const bool lightChanged = renderLight.impl != updateParameters.light;
+
+ if (lightChanged) {
+ renderLight.impl = updateParameters.light;
+ renderLight.transition(transitionParameters);
+ }
+
+ if (lightChanged || zoomChanged || renderLight.hasTransition()) {
+ renderLight.evaluate(evaluationParameters);
+ }
+
+
+ const ImageDifference imageDiff = diffImages(imageImpls, updateParameters.images);
+ imageImpls = updateParameters.images;
+
+ // Only trigger tile reparse for changed images. Changed images only need a relayout when they have a different size.
+ bool hasImageDiff = !imageDiff.removed.empty();
+
+ // Remove removed images from sprite atlas.
+ for (const auto& entry : imageDiff.removed) {
+ imageManager->removeImage(entry.first);
+ patternAtlas->removePattern(entry.first);
+ }
+
+ // Add added images to sprite atlas.
+ for (const auto& entry : imageDiff.added) {
+ imageManager->addImage(entry.second);
+ }
+
+ // Update changed images.
+ for (const auto& entry : imageDiff.changed) {
+ if (imageManager->updateImage(entry.second.after)) {
+ patternAtlas->removePattern(entry.first);
+ hasImageDiff = true;
+ }
+ }
+
+ imageManager->notifyIfMissingImageAdded();
+ imageManager->setLoaded(updateParameters.spriteLoaded);
+
+ const LayerDifference layerDiff = diffLayers(layerImpls, updateParameters.layers);
+ layerImpls = updateParameters.layers;
+
+ // Remove render layers for removed layers.
+ for (const auto& entry : layerDiff.removed) {
+ renderLayers.erase(entry.first);
+ }
+
+ // Create render layers for newly added layers.
+ for (const auto& entry : layerDiff.added) {
+ auto renderLayer = LayerManager::get()->createRenderLayer(entry.second);
+ renderLayer->transition(transitionParameters);
+ renderLayers.emplace(entry.first, std::move(renderLayer));
+ }
+
+ // Update render layers for changed layers.
+ for (const auto& entry : layerDiff.changed) {
+ renderLayers.at(entry.first)->transition(transitionParameters, entry.second.after);
+ }
+
+ if (!layerDiff.removed.empty() || !layerDiff.added.empty() || !layerDiff.changed.empty()) {
+ glyphManager->evict(fontStacks(*updateParameters.layers));
+ }
+
+ // Update layers for class and zoom changes.
+ std::unordered_set<std::string> constantsMaskChanged;
+ for (const auto& entry : renderLayers) {
+ RenderLayer& layer = *entry.second;
+ const bool layerAddedOrChanged = layerDiff.added.count(entry.first) || layerDiff.changed.count(entry.first);
+ if (layerAddedOrChanged || zoomChanged || layer.hasTransition() || layer.hasCrossfade()) {
+ auto previousMask = layer.evaluatedProperties->constantsMask();
+ layer.evaluate(evaluationParameters);
+ if (previousMask != layer.evaluatedProperties->constantsMask()) {
+ constantsMaskChanged.insert(layer.getID());
+ }
+ }
+ }
+
+ const SourceDifference sourceDiff = diffSources(sourceImpls, updateParameters.sources);
+ sourceImpls = updateParameters.sources;
+
+ // Remove render layers for removed sources.
+ for (const auto& entry : sourceDiff.removed) {
+ renderSources.erase(entry.first);
+ }
+
+ // Create render sources for newly added sources.
+ for (const auto& entry : sourceDiff.added) {
+ std::unique_ptr<RenderSource> renderSource = RenderSource::create(entry.second);
+ renderSource->setObserver(this);
+ renderSources.emplace(entry.first, std::move(renderSource));
+ }
+ transformState = updateParameters.transformState;
+
+ // Create parameters for the render tree.
+ auto renderTreeParameters = std::make_unique<RenderTreeParameters>(
+ updateParameters.transformState,
+ updateParameters.mode,
+ updateParameters.debugOptions,
+ updateParameters.timePoint,
+ renderLight.getEvaluated());
+
+ std::set<LayerRenderItem> layerRenderItems;
+ std::vector<std::reference_wrapper<RenderLayer>> layersNeedPlacement;
+ auto renderItemsEmplaceHint = layerRenderItems.begin();
+
+ // Update all sources and initialize renderItems.
+ for (const auto& sourceImpl : *sourceImpls) {
+ RenderSource* source = renderSources.at(sourceImpl->id).get();
+ std::vector<Immutable<LayerProperties>> filteredLayersForSource;
+ filteredLayersForSource.reserve(layerImpls->size());
+ bool sourceNeedsRendering = false;
+ bool sourceNeedsRelayout = false;
+
+ uint32_t index = 0u;
+ const auto begin = layerImpls->begin();
+ const auto end = layerImpls->end();
+ for (auto it = begin; it != end; ++it, ++index) {
+ const Immutable<Layer::Impl>& layerImpl = *it;
+ RenderLayer* layer = getRenderLayer(layerImpl->id);
+ const auto* layerInfo = layerImpl->getTypeInfo();
+ const bool layerNeedsRendering = layer->needsRendering();
+ const bool zoomFitsLayer = layer->supportsZoom(zoomHistory.lastZoom);
+ renderTreeParameters->has3D |= (layerInfo->pass3d == LayerTypeInfo::Pass3D::Required);
+
+ if (layerInfo->source != LayerTypeInfo::Source::NotRequired) {
+ if (layerImpl->source == sourceImpl->id) {
+ sourceNeedsRelayout = (sourceNeedsRelayout || hasImageDiff || constantsMaskChanged.count(layerImpl->id) || hasLayoutDifference(layerDiff, layerImpl->id));
+ if (layerNeedsRendering) {
+ filteredLayersForSource.push_back(layer->evaluatedProperties);
+ if (zoomFitsLayer) {
+ sourceNeedsRendering = true;
+ renderItemsEmplaceHint = layerRenderItems.emplace_hint(renderItemsEmplaceHint, *layer, source, index);
+ }
+ }
+ }
+ continue;
+ }
+
+ // Handle layers without source.
+ if (layerNeedsRendering && zoomFitsLayer && sourceImpl.get() == sourceImpls->at(0).get()) {
+ if (backgroundLayerAsColor && layerImpl.get() == layerImpls->at(0).get()) {
+ const auto& solidBackground = layer->getSolidBackground();
+ if (solidBackground) {
+ renderTreeParameters->backgroundColor = *solidBackground;
+ continue; // This layer is shown with background color, and it shall not be added to render items.
+ }
+ }
+ renderItemsEmplaceHint = layerRenderItems.emplace_hint(renderItemsEmplaceHint, *layer, nullptr, index);
+ }
+ }
+ source->update(sourceImpl,
+ filteredLayersForSource,
+ sourceNeedsRendering,
+ sourceNeedsRelayout,
+ tileParameters);
+ }
+
+ renderTreeParameters->loaded = updateParameters.styleLoaded && isLoaded();
+ if (!isMapModeContinuous && !renderTreeParameters->loaded) {
+ return nullptr;
+ }
+
+ std::vector<SourceRenderItem> sourceRenderItems;
+ // Update all matrices and generate data that we should upload to the GPU.
+ for (const auto& entry : renderSources) {
+ if (entry.second->isEnabled()) {
+ entry.second->prepare({renderTreeParameters->transformParams, updateParameters.debugOptions});
+ sourceRenderItems.emplace_back(*entry.second);
+ }
+ }
+
+ for (auto& renderItem : layerRenderItems) {
+ RenderLayer& renderLayer = renderItem.layer;
+ renderLayer.prepare({renderItem.source, *imageManager, *patternAtlas, *lineAtlas, updateParameters.transformState});
+ if (renderLayer.needsPlacement()) {
+ layersNeedPlacement.emplace_back(renderLayer);
+ }
+ }
+
+ {
+ if (!isMapModeContinuous) {
+ // TODO: Think about right way for symbol index to handle still rendering
+ crossTileSymbolIndex.reset();
+ }
+
+ bool symbolBucketsChanged = false;
+ const bool placementChanged = !placement->stillRecent(updateParameters.timePoint);
+ std::set<std::string> usedSymbolLayers;
+ if (placementChanged) {
+ placement = std::make_unique<Placement>(
+ updateParameters.transformState, updateParameters.mode,
+ updateParameters.transitionOptions, updateParameters.crossSourceCollisions,
+ std::move(placement));
+ }
+
+ for (auto it = layersNeedPlacement.rbegin(); it != layersNeedPlacement.rend(); ++it) {
+ const RenderLayer& layer = *it;
+ if (crossTileSymbolIndex.addLayer(layer, updateParameters.transformState.getLatLng().longitude())) symbolBucketsChanged = true;
+
+ if (placementChanged) {
+ usedSymbolLayers.insert(layer.getID());
+ placement->placeLayer(layer, renderTreeParameters->transformParams.projMatrix, updateParameters.debugOptions & MapDebugOptions::Collision);
+ }
+ }
+
+ if (placementChanged) {
+ placement->commit(updateParameters.timePoint);
+ crossTileSymbolIndex.pruneUnusedLayers(usedSymbolLayers);
+ for (const auto& entry : renderSources) {
+ entry.second->updateFadingTiles();
+ }
+ } else {
+ placement->setStale();
+ }
+
+ for (auto it = layersNeedPlacement.rbegin(); it != layersNeedPlacement.rend(); ++it) {
+ placement->updateLayerBuckets(*it, updateParameters.transformState, placementChanged || symbolBucketsChanged);
+ }
+
+ renderTreeParameters->symbolFadeChange = placement->symbolFadeChange(updateParameters.timePoint);
+ }
+
+ renderTreeParameters->needsRepaint = isMapModeContinuous && hasTransitions(updateParameters.timePoint);
+ if (!renderTreeParameters->needsRepaint && renderTreeParameters->loaded) {
+ // Notify observer about unused images when map is fully loaded
+ // and there are no ongoing transitions.
+ imageManager->reduceMemoryUseIfCacheSizeExceedsLimit();
+ }
+
+ return std::make_unique<RenderTreeImpl>(
+ std::move(renderTreeParameters),
+ std::move(layerRenderItems),
+ std::move(sourceRenderItems),
+ *lineAtlas,
+ *patternAtlas);
+}
+
+std::vector<Feature> RenderOrchestrator::queryRenderedFeatures(const ScreenLineString& geometry, const RenderedQueryOptions& options) const {
+ std::vector<const RenderLayer*> layers;
+ if (options.layerIDs) {
+ for (const auto& layerID : *options.layerIDs) {
+ if (const RenderLayer* layer = getRenderLayer(layerID)) {
+ layers.emplace_back(layer);
+ }
+ }
+ } else {
+ for (const auto& entry : renderLayers) {
+ layers.emplace_back(entry.second.get());
+ }
+ }
+
+ return queryRenderedFeatures(geometry, options, layers);
+}
+
+void RenderOrchestrator::queryRenderedSymbols(std::unordered_map<std::string, std::vector<Feature>>& resultsByLayer,
+ const ScreenLineString& geometry,
+ const std::vector<const RenderLayer*>& layers,
+ const RenderedQueryOptions& options) const {
+
+ auto renderedSymbols = placement->getCollisionIndex().queryRenderedSymbols(geometry);
+ std::vector<std::reference_wrapper<const RetainedQueryData>> bucketQueryData;
+ for (auto entry : renderedSymbols) {
+ bucketQueryData.emplace_back(placement->getQueryData(entry.first));
+ }
+ // Although symbol query is global, symbol results are only sortable within a bucket
+ // For a predictable global sort renderItems, we sort the buckets based on their corresponding tile position
+ std::sort(bucketQueryData.begin(), bucketQueryData.end(), [](const RetainedQueryData& a, const RetainedQueryData& b) {
+ return
+ std::tie(a.tileID.canonical.z, a.tileID.canonical.y, a.tileID.wrap, a.tileID.canonical.x) <
+ std::tie(b.tileID.canonical.z, b.tileID.canonical.y, b.tileID.wrap, b.tileID.canonical.x);
+ });
+
+ for (auto wrappedQueryData : bucketQueryData) {
+ auto& queryData = wrappedQueryData.get();
+ auto bucketSymbols = queryData.featureIndex->lookupSymbolFeatures(renderedSymbols[queryData.bucketInstanceId],
+ options,
+ layers,
+ queryData.tileID,
+ queryData.featureSortOrder);
+
+ for (auto layer : bucketSymbols) {
+ auto& resultFeatures = resultsByLayer[layer.first];
+ std::move(layer.second.begin(), layer.second.end(), std::inserter(resultFeatures, resultFeatures.end()));
+ }
+ }
+}
+
+std::vector<Feature> RenderOrchestrator::queryRenderedFeatures(const ScreenLineString& geometry, const RenderedQueryOptions& options, const std::vector<const RenderLayer*>& layers) const {
+ std::unordered_set<std::string> sourceIDs;
+ for (const RenderLayer* layer : layers) {
+ sourceIDs.emplace(layer->baseImpl->source);
+ }
+
+ mat4 projMatrix;
+ transformState.getProjMatrix(projMatrix);
+
+ std::unordered_map<std::string, std::vector<Feature>> resultsByLayer;
+ for (const auto& sourceID : sourceIDs) {
+ if (RenderSource* renderSource = getRenderSource(sourceID)) {
+ auto sourceResults = renderSource->queryRenderedFeatures(geometry, transformState, layers, options, projMatrix);
+ std::move(sourceResults.begin(), sourceResults.end(), std::inserter(resultsByLayer, resultsByLayer.begin()));
+ }
+ }
+
+ queryRenderedSymbols(resultsByLayer, geometry, layers, options);
+
+ std::vector<Feature> result;
+
+ if (resultsByLayer.empty()) {
+ return result;
+ }
+
+ // Combine all results based on the style layer renderItems.
+ for (const auto& layerImpl : *layerImpls) {
+ const RenderLayer* layer = getRenderLayer(layerImpl->id);
+ if (!layer->needsRendering() || !layer->supportsZoom(zoomHistory.lastZoom)) {
+ continue;
+ }
+
+ auto it = resultsByLayer.find(layer->baseImpl->id);
+ if (it != resultsByLayer.end()) {
+ std::move(it->second.begin(), it->second.end(), std::back_inserter(result));
+ }
+ }
+
+ return result;
+}
+
+std::vector<Feature> RenderOrchestrator::queryShapeAnnotations(const ScreenLineString& geometry) const {
+ assert(LayerManager::annotationsEnabled);
+ std::vector<const RenderLayer*> shapeAnnotationLayers;
+ RenderedQueryOptions options;
+ for (const auto& layerImpl : *layerImpls) {
+ if (std::mismatch(layerImpl->id.begin(), layerImpl->id.end(),
+ AnnotationManager::ShapeLayerID.begin(), AnnotationManager::ShapeLayerID.end()).second == AnnotationManager::ShapeLayerID.end()) {
+ if (const RenderLayer* layer = getRenderLayer(layerImpl->id)) {
+ shapeAnnotationLayers.emplace_back(layer);
+ }
+ }
+ }
+
+ return queryRenderedFeatures(geometry, options, shapeAnnotationLayers);
+}
+
+std::vector<Feature> RenderOrchestrator::querySourceFeatures(const std::string& sourceID, const SourceQueryOptions& options) const {
+ const RenderSource* source = getRenderSource(sourceID);
+ if (!source) return {};
+
+ return source->querySourceFeatures(options);
+}
+
+FeatureExtensionValue RenderOrchestrator::queryFeatureExtensions(const std::string& sourceID,
+ const Feature& feature,
+ const std::string& extension,
+ const std::string& extensionField,
+ const optional<std::map<std::string, Value>>& args) const {
+ if (RenderSource* renderSource = getRenderSource(sourceID)) {
+ return renderSource->queryFeatureExtensions(feature, extension, extensionField, args);
+ }
+ return {};
+}
+
+void RenderOrchestrator::reduceMemoryUse() {
+ for (const auto& entry : renderSources) {
+ entry.second->reduceMemoryUse();
+ }
+ imageManager->reduceMemoryUse();
+ observer->onInvalidate();
+}
+
+void RenderOrchestrator::dumpDebugLogs() {
+ for (const auto& entry : renderSources) {
+ entry.second->dumpDebugLogs();
+ }
+
+ imageManager->dumpDebugLogs();
+}
+
+RenderLayer* RenderOrchestrator::getRenderLayer(const std::string& id) {
+ auto it = renderLayers.find(id);
+ return it != renderLayers.end() ? it->second.get() : nullptr;
+}
+
+const RenderLayer* RenderOrchestrator::getRenderLayer(const std::string& id) const {
+ auto it = renderLayers.find(id);
+ return it != renderLayers.end() ? it->second.get() : nullptr;
+}
+
+RenderSource* RenderOrchestrator::getRenderSource(const std::string& id) const {
+ auto it = renderSources.find(id);
+ return it != renderSources.end() ? it->second.get() : nullptr;
+}
+
+bool RenderOrchestrator::hasTransitions(TimePoint timePoint) const {
+ if (renderLight.hasTransition()) {
+ return true;
+ }
+
+ for (const auto& entry : renderLayers) {
+ if (entry.second->hasTransition()) {
+ return true;
+ }
+ }
+
+ if (placement->hasTransitions(timePoint)) {
+ return true;
+ }
+
+ for (const auto& entry : renderSources) {
+ if (entry.second->hasFadingTiles()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool RenderOrchestrator::isLoaded() const {
+ for (const auto& entry: renderSources) {
+ if (!entry.second->isLoaded()) {
+ return false;
+ }
+ }
+
+ if (!imageManager->isLoaded()) {
+ return false;
+ }
+
+ return true;
+}
+
+void RenderOrchestrator::onGlyphsError(const FontStack& fontStack, const GlyphRange& glyphRange, std::exception_ptr error) {
+ Log::Error(Event::Style, "Failed to load glyph range %d-%d for font stack %s: %s",
+ glyphRange.first, glyphRange.second, fontStackToString(fontStack).c_str(), util::toString(error).c_str());
+ observer->onResourceError(error);
+}
+
+void RenderOrchestrator::onTileError(RenderSource& source, const OverscaledTileID& tileID, std::exception_ptr error) {
+ Log::Error(Event::Style, "Failed to load tile %s for source %s: %s",
+ util::toString(tileID).c_str(), source.baseImpl->id.c_str(), util::toString(error).c_str());
+ observer->onResourceError(error);
+}
+
+void RenderOrchestrator::onTileChanged(RenderSource&, const OverscaledTileID&) {
+ observer->onInvalidate();
+}
+
+void RenderOrchestrator::onStyleImageMissing(const std::string& id, std::function<void()> done) {
+ observer->onStyleImageMissing(id, std::move(done));
+}
+
+void RenderOrchestrator::onRemoveUnusedStyleImages(const std::vector<std::string>& unusedImageIDs) {
+ observer->onRemoveUnusedStyleImages(unusedImageIDs);
+}
+
+} // namespace mbgl
diff --git a/src/mbgl/renderer/render_orchestrator.hpp b/src/mbgl/renderer/render_orchestrator.hpp
new file mode 100644
index 0000000000..a60bac2e16
--- /dev/null
+++ b/src/mbgl/renderer/render_orchestrator.hpp
@@ -0,0 +1,118 @@
+#pragma once
+
+#include <mbgl/renderer/renderer.hpp>
+#include <mbgl/renderer/render_source_observer.hpp>
+#include <mbgl/renderer/render_light.hpp>
+#include <mbgl/style/image.hpp>
+#include <mbgl/style/source.hpp>
+#include <mbgl/style/layer.hpp>
+#include <mbgl/map/transform_state.hpp>
+#include <mbgl/map/zoom_history.hpp>
+#include <mbgl/text/cross_tile_symbol_index.hpp>
+#include <mbgl/text/glyph_manager_observer.hpp>
+#include <mbgl/renderer/image_manager_observer.hpp>
+#include <mbgl/text/placement.hpp>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace mbgl {
+
+class RendererObserver;
+class RenderSource;
+class RenderLayer;
+class UpdateParameters;
+class RenderStaticData;
+class RenderedQueryOptions;
+class SourceQueryOptions;
+class GlyphManager;
+class ImageManager;
+class LineAtlas;
+class PatternAtlas;
+class CrossTileSymbolIndex;
+class RenderTree;
+
+class RenderOrchestrator final : public GlyphManagerObserver,
+ public ImageManagerObserver,
+ public RenderSourceObserver {
+public:
+ RenderOrchestrator(
+ bool backgroundLayerAsColor_,
+ optional<std::string> localFontFamily_);
+ ~RenderOrchestrator() override;
+
+ void markContextLost() {
+ contextLost = true;
+ };
+ // TODO: Introduce RenderOrchestratorObserver.
+ void setObserver(RendererObserver*);
+
+ std::unique_ptr<RenderTree> createRenderTree(const UpdateParameters&);
+
+ std::vector<Feature> queryRenderedFeatures(const ScreenLineString&, const RenderedQueryOptions&) const;
+ std::vector<Feature> querySourceFeatures(const std::string& sourceID, const SourceQueryOptions&) const;
+ std::vector<Feature> queryShapeAnnotations(const ScreenLineString&) const;
+
+ FeatureExtensionValue queryFeatureExtensions(const std::string& sourceID,
+ const Feature& feature,
+ const std::string& extension,
+ const std::string& extensionField,
+ const optional<std::map<std::string, Value>>& args) const;
+
+ void reduceMemoryUse();
+ void dumpDebugLogs();
+
+private:
+ bool isLoaded() const;
+ bool hasTransitions(TimePoint) const;
+
+ RenderSource* getRenderSource(const std::string& id) const;
+
+ RenderLayer* getRenderLayer(const std::string& id);
+ const RenderLayer* getRenderLayer(const std::string& id) const;
+
+ void queryRenderedSymbols(std::unordered_map<std::string, std::vector<Feature>>& resultsByLayer,
+ const ScreenLineString& geometry,
+ const std::vector<const RenderLayer*>& layers,
+ const RenderedQueryOptions& options) const;
+
+ std::vector<Feature> queryRenderedFeatures(const ScreenLineString&, const RenderedQueryOptions&, const std::vector<const RenderLayer*>&) const;
+
+ // GlyphManagerObserver implementation.
+ void onGlyphsError(const FontStack&, const GlyphRange&, std::exception_ptr) override;
+
+ // RenderSourceObserver implementation.
+ void onTileChanged(RenderSource&, const OverscaledTileID&) override;
+ void onTileError(RenderSource&, const OverscaledTileID&, std::exception_ptr) override;
+
+ // ImageManagerObserver implementation
+ void onStyleImageMissing(const std::string&, std::function<void()>) override;
+ void onRemoveUnusedStyleImages(const std::vector<std::string>&) override;
+
+ RendererObserver* observer;
+
+ ZoomHistory zoomHistory;
+ TransformState transformState;
+
+ std::unique_ptr<GlyphManager> glyphManager;
+ std::unique_ptr<ImageManager> imageManager;
+ std::unique_ptr<LineAtlas> lineAtlas;
+ std::unique_ptr<PatternAtlas> patternAtlas;
+
+ Immutable<std::vector<Immutable<style::Image::Impl>>> imageImpls;
+ Immutable<std::vector<Immutable<style::Source::Impl>>> sourceImpls;
+ Immutable<std::vector<Immutable<style::Layer::Impl>>> layerImpls;
+
+ std::unordered_map<std::string, std::unique_ptr<RenderSource>> renderSources;
+ std::unordered_map<std::string, std::unique_ptr<RenderLayer>> renderLayers;
+ RenderLight renderLight;
+
+ CrossTileSymbolIndex crossTileSymbolIndex;
+ std::unique_ptr<Placement> placement;
+
+ const bool backgroundLayerAsColor;
+ bool contextLost = false;
+};
+
+} // namespace mbgl
diff --git a/src/mbgl/renderer/render_pass.hpp b/src/mbgl/renderer/render_pass.hpp
index ee0a846d7e..5d18304129 100644
--- a/src/mbgl/renderer/render_pass.hpp
+++ b/src/mbgl/renderer/render_pass.hpp
@@ -12,7 +12,6 @@ enum class RenderPass : uint8_t {
Opaque = 1 << 0,
Translucent = 1 << 1,
Pass3D = 1 << 2,
- Upload = 1 << 3,
};
MBGL_CONSTEXPR RenderPass operator|(RenderPass a, RenderPass b) {
diff --git a/src/mbgl/renderer/render_tree.hpp b/src/mbgl/renderer/render_tree.hpp
new file mode 100644
index 0000000000..4266ddec6d
--- /dev/null
+++ b/src/mbgl/renderer/render_tree.hpp
@@ -0,0 +1,78 @@
+#pragma once
+
+#include <mbgl/renderer/paint_parameters.hpp>
+
+#include <cassert>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace mbgl {
+
+class PaintParameters;
+class PatternAtlas;
+
+namespace gfx {
+class UploadPass;
+} // namespace gfx
+
+class RenderItem {
+public:
+ virtual ~RenderItem() = default;
+ virtual void upload(gfx::UploadPass&) const = 0;
+ virtual void render(PaintParameters&) const = 0;
+ virtual bool hasRenderPass(RenderPass) const = 0;
+ virtual const std::string& getName() const = 0;
+};
+
+using RenderItems = std::vector<std::reference_wrapper<const RenderItem>>;
+
+class RenderTreeParameters {
+public:
+ explicit RenderTreeParameters(
+ const TransformState& state_,
+ MapMode mapMode_,
+ MapDebugOptions debugOptions_,
+ TimePoint timePoint_,
+ const EvaluatedLight& light_)
+ : transformParams(state_),
+ mapMode(mapMode_),
+ debugOptions(debugOptions_),
+ timePoint(timePoint_),
+ light(light_) {
+ }
+ TransformParameters transformParams;
+ MapMode mapMode;
+ MapDebugOptions debugOptions;
+ TimePoint timePoint;
+ EvaluatedLight light;
+ bool has3D = false;
+ Color backgroundColor;
+ float symbolFadeChange = 0.0f;
+ bool needsRepaint = false;
+ bool loaded = false;
+};
+
+class RenderTree {
+public:
+ virtual ~RenderTree() = default;
+ // Render items
+ virtual RenderItems getLayerRenderItems() const = 0;
+ virtual RenderItems getSourceRenderItems() const = 0;
+ // Resources
+ virtual LineAtlas& getLineAtlas() const = 0;
+ virtual PatternAtlas& getPatternAtlas() const = 0;
+ // Parameters
+ const RenderTreeParameters& getParameters() const {
+ return *parameters;
+ }
+protected:
+ RenderTree(std::unique_ptr<RenderTreeParameters> parameters_)
+ : parameters(std::move(parameters_)) {
+ assert(parameters);
+ }
+ std::unique_ptr<RenderTreeParameters> parameters;
+};
+
+
+} // namespace mbgl
diff --git a/src/mbgl/renderer/renderer.cpp b/src/mbgl/renderer/renderer.cpp
index ba213c435d..76e618a50f 100644
--- a/src/mbgl/renderer/renderer.cpp
+++ b/src/mbgl/renderer/renderer.cpp
@@ -2,6 +2,7 @@
#include <mbgl/layermanager/layer_manager.hpp>
#include <mbgl/renderer/renderer_impl.hpp>
+#include <mbgl/renderer/render_tree.hpp>
#include <mbgl/gfx/backend_scope.hpp>
#include <mbgl/annotation/annotation_manager.hpp>
@@ -23,27 +24,30 @@ Renderer::~Renderer() {
}
void Renderer::markContextLost() {
- impl->markContextLost();
+ impl->orchestrator.markContextLost();
}
void Renderer::setObserver(RendererObserver* observer) {
impl->setObserver(observer);
+ impl->orchestrator.setObserver(observer);
}
void Renderer::render(const UpdateParameters& updateParameters) {
- impl->render(updateParameters);
+ if (auto renderTree = impl->orchestrator.createRenderTree(updateParameters)) {
+ impl->render(*renderTree);
+ }
}
std::vector<Feature> Renderer::queryRenderedFeatures(const ScreenLineString& geometry, const RenderedQueryOptions& options) const {
- return impl->queryRenderedFeatures(geometry, options);
+ return impl->orchestrator.queryRenderedFeatures(geometry, options);
}
std::vector<Feature> Renderer::queryRenderedFeatures(const ScreenCoordinate& point, const RenderedQueryOptions& options) const {
- return impl->queryRenderedFeatures({ point }, options);
+ return impl->orchestrator.queryRenderedFeatures({ point }, options);
}
std::vector<Feature> Renderer::queryRenderedFeatures(const ScreenBox& box, const RenderedQueryOptions& options) const {
- return impl->queryRenderedFeatures(
+ return impl->orchestrator.queryRenderedFeatures(
{
box.min,
{box.max.x, box.min.y},
@@ -69,7 +73,7 @@ AnnotationIDs Renderer::queryShapeAnnotations(const ScreenBox& box) const {
if (!LayerManager::annotationsEnabled) {
return {};
}
- auto features = impl->queryShapeAnnotations({
+ auto features = impl->orchestrator.queryShapeAnnotations({
box.min,
{box.max.x, box.min.y},
box.max,
@@ -96,7 +100,7 @@ AnnotationIDs Renderer::getAnnotationIDs(const std::vector<Feature>& features) c
}
std::vector<Feature> Renderer::querySourceFeatures(const std::string& sourceID, const SourceQueryOptions& options) const {
- return impl->querySourceFeatures(sourceID, options);
+ return impl->orchestrator.querySourceFeatures(sourceID, options);
}
FeatureExtensionValue Renderer::queryFeatureExtensions(const std::string& sourceID,
@@ -104,16 +108,16 @@ FeatureExtensionValue Renderer::queryFeatureExtensions(const std::string& source
const std::string& extension,
const std::string& extensionField,
const optional<std::map<std::string, Value>>& args) const {
- return impl->queryFeatureExtensions(sourceID, feature, extension, extensionField, args);
+ return impl->orchestrator.queryFeatureExtensions(sourceID, feature, extension, extensionField, args);
}
void Renderer::dumpDebugLogs() {
- impl->dumpDebugLogs();
+ impl->orchestrator.dumpDebugLogs();
}
void Renderer::reduceMemoryUse() {
- gfx::BackendScope guard { impl->backend };
impl->reduceMemoryUse();
+ impl->orchestrator.reduceMemoryUse();
}
} // namespace mbgl
diff --git a/src/mbgl/renderer/renderer_impl.cpp b/src/mbgl/renderer/renderer_impl.cpp
index c7cca3a659..c536497c30 100644
--- a/src/mbgl/renderer/renderer_impl.cpp
+++ b/src/mbgl/renderer/renderer_impl.cpp
@@ -1,34 +1,17 @@
-#include <mbgl/annotation/annotation_manager.hpp>
-#include <mbgl/layermanager/layer_manager.hpp>
#include <mbgl/renderer/renderer_impl.hpp>
-#include <mbgl/renderer/renderer_observer.hpp>
-#include <mbgl/renderer/render_source.hpp>
-#include <mbgl/renderer/render_layer.hpp>
-#include <mbgl/renderer/render_static_data.hpp>
-#include <mbgl/renderer/update_parameters.hpp>
-#include <mbgl/renderer/upload_parameters.hpp>
-#include <mbgl/renderer/pattern_atlas.hpp>
-#include <mbgl/renderer/paint_parameters.hpp>
-#include <mbgl/renderer/transition_parameters.hpp>
-#include <mbgl/renderer/property_evaluation_parameters.hpp>
-#include <mbgl/renderer/tile_parameters.hpp>
-#include <mbgl/renderer/render_tile.hpp>
-#include <mbgl/renderer/style_diff.hpp>
-#include <mbgl/renderer/query.hpp>
+
+#include <mbgl/geometry/line_atlas.hpp>
#include <mbgl/gfx/backend_scope.hpp>
-#include <mbgl/renderer/image_manager.hpp>
#include <mbgl/gfx/renderer_backend.hpp>
#include <mbgl/gfx/upload_pass.hpp>
#include <mbgl/gfx/render_pass.hpp>
#include <mbgl/gfx/cull_face_mode.hpp>
#include <mbgl/gfx/context.hpp>
#include <mbgl/gfx/renderable.hpp>
-#include <mbgl/geometry/line_atlas.hpp>
-#include <mbgl/style/source_impl.hpp>
-#include <mbgl/style/transition_options.hpp>
-#include <mbgl/text/glyph_manager.hpp>
-#include <mbgl/tile/tile.hpp>
-#include <mbgl/util/math.hpp>
+#include <mbgl/renderer/pattern_atlas.hpp>
+#include <mbgl/renderer/renderer_observer.hpp>
+#include <mbgl/renderer/render_static_data.hpp>
+#include <mbgl/renderer/render_tree.hpp>
#include <mbgl/util/string.hpp>
#include <mbgl/util/logging.hpp>
@@ -43,317 +26,36 @@ static RendererObserver& nullObserver() {
Renderer::Impl::Impl(gfx::RendererBackend& backend_,
float pixelRatio_,
- const optional<std::string> programCacheDir_,
- const optional<std::string> localFontFamily_)
- : backend(backend_)
+ optional<std::string> programCacheDir_,
+ optional<std::string> localFontFamily_)
+ : orchestrator(!backend_.contextIsShared(), std::move(localFontFamily_))
+ , backend(backend_)
, observer(&nullObserver())
, pixelRatio(pixelRatio_)
- , programCacheDir(std::move(programCacheDir_))
- , localFontFamily(std::move(localFontFamily_))
- , glyphManager(std::make_unique<GlyphManager>(std::make_unique<LocalGlyphRasterizer>(localFontFamily)))
- , imageManager(std::make_unique<ImageManager>())
- , lineAtlas(std::make_unique<LineAtlas>(Size{ 256, 512 }))
- , patternAtlas(std::make_unique<PatternAtlas>())
- , imageImpls(makeMutable<std::vector<Immutable<style::Image::Impl>>>())
- , sourceImpls(makeMutable<std::vector<Immutable<style::Source::Impl>>>())
- , layerImpls(makeMutable<std::vector<Immutable<style::Layer::Impl>>>())
- , renderLight(makeMutable<Light::Impl>())
- , placement(std::make_unique<Placement>(TransformState{}, MapMode::Static, TransitionOptions{}, true)) {
- glyphManager->setObserver(this);
- imageManager->setObserver(this);
+ , programCacheDir(std::move(programCacheDir_)) {
+
}
Renderer::Impl::~Impl() {
assert(gfx::BackendScope::exists());
-
- if (contextLost) {
- // Signal all RenderLayers that the context was lost
- // before cleaning up. At the moment, only CustomLayer is
- // interested whether rendering context is lost. However, it would be
- // beneficial for dynamically loaded or other custom built-in plugins.
- for (const auto& entry : renderLayers) {
- RenderLayer& layer = *entry.second;
- layer.markContextDestroyed();
- }
- }
};
void Renderer::Impl::setObserver(RendererObserver* observer_) {
observer = observer_ ? observer_ : &nullObserver();
}
-void Renderer::Impl::render(const UpdateParameters& updateParameters) {
- const bool isMapModeContinuous = updateParameters.mode == MapMode::Continuous;
- if (!isMapModeContinuous) {
- // Reset zoom history state.
- zoomHistory.first = true;
- }
-
- assert(gfx::BackendScope::exists());
- if (LayerManager::annotationsEnabled) {
- updateParameters.annotationManager.updateData();
- }
-
- const bool zoomChanged = zoomHistory.update(updateParameters.transformState.getZoom(), updateParameters.timePoint);
-
- const TransitionOptions transitionOptions = isMapModeContinuous ? updateParameters.transitionOptions : TransitionOptions();
-
- const TransitionParameters transitionParameters {
- updateParameters.timePoint,
- transitionOptions
- };
-
- const PropertyEvaluationParameters evaluationParameters {
- zoomHistory,
- updateParameters.timePoint,
- transitionOptions.duration.value_or(isMapModeContinuous ? util::DEFAULT_TRANSITION_DURATION : Duration::zero())
- };
-
- const TileParameters tileParameters {
- updateParameters.pixelRatio,
- updateParameters.debugOptions,
- updateParameters.transformState,
- updateParameters.fileSource,
- updateParameters.mode,
- updateParameters.annotationManager,
- *imageManager,
- *glyphManager,
- updateParameters.prefetchZoomDelta
- };
-
- glyphManager->setURL(updateParameters.glyphURL);
-
- // Update light.
- const bool lightChanged = renderLight.impl != updateParameters.light;
-
- if (lightChanged) {
- renderLight.impl = updateParameters.light;
- renderLight.transition(transitionParameters);
- }
-
- if (lightChanged || zoomChanged || renderLight.hasTransition()) {
- renderLight.evaluate(evaluationParameters);
- }
-
-
- const ImageDifference imageDiff = diffImages(imageImpls, updateParameters.images);
- imageImpls = updateParameters.images;
-
- // Only trigger tile reparse for changed images. Changed images only need a relayout when they have a different size.
- bool hasImageDiff = !imageDiff.removed.empty();
-
- // Remove removed images from sprite atlas.
- for (const auto& entry : imageDiff.removed) {
- imageManager->removeImage(entry.first);
- patternAtlas->removePattern(entry.first);
- }
-
- // Add added images to sprite atlas.
- for (const auto& entry : imageDiff.added) {
- imageManager->addImage(entry.second);
- }
-
- // Update changed images.
- for (const auto& entry : imageDiff.changed) {
- if (imageManager->updateImage(entry.second.after)) {
- patternAtlas->removePattern(entry.first);
- hasImageDiff = true;
- }
- }
-
- imageManager->notifyIfMissingImageAdded();
- imageManager->setLoaded(updateParameters.spriteLoaded);
-
- const LayerDifference layerDiff = diffLayers(layerImpls, updateParameters.layers);
- layerImpls = updateParameters.layers;
-
- // Remove render layers for removed layers.
- for (const auto& entry : layerDiff.removed) {
- renderLayers.erase(entry.first);
- }
-
- // Create render layers for newly added layers.
- for (const auto& entry : layerDiff.added) {
- auto renderLayer = LayerManager::get()->createRenderLayer(entry.second);
- renderLayer->transition(transitionParameters);
- renderLayers.emplace(entry.first, std::move(renderLayer));
- }
-
- // Update render layers for changed layers.
- for (const auto& entry : layerDiff.changed) {
- renderLayers.at(entry.first)->transition(transitionParameters, entry.second.after);
- }
-
- if (!layerDiff.removed.empty() || !layerDiff.added.empty() || !layerDiff.changed.empty()) {
- glyphManager->evict(fontStacks(*updateParameters.layers));
- }
-
- // Update layers for class and zoom changes.
- for (const auto& entry : renderLayers) {
- RenderLayer& layer = *entry.second;
- const bool layerAddedOrChanged = layerDiff.added.count(entry.first) || layerDiff.changed.count(entry.first);
- if (layerAddedOrChanged || zoomChanged || layer.hasTransition() || layer.hasCrossfade()) {
- layer.evaluate(evaluationParameters);
- }
- }
-
-
- const SourceDifference sourceDiff = diffSources(sourceImpls, updateParameters.sources);
- sourceImpls = updateParameters.sources;
-
- // Remove render layers for removed sources.
- for (const auto& entry : sourceDiff.removed) {
- renderSources.erase(entry.first);
- }
-
- // Create render sources for newly added sources.
- for (const auto& entry : sourceDiff.added) {
- std::unique_ptr<RenderSource> renderSource = RenderSource::create(entry.second);
- renderSource->setObserver(this);
- renderSources.emplace(entry.first, std::move(renderSource));
- }
- transformState = updateParameters.transformState;
-
- if (!staticData) {
- staticData = std::make_unique<RenderStaticData>(backend.getContext(), pixelRatio, programCacheDir);
- }
-
- Color backgroundColor;
-
- struct RenderItem {
- RenderItem(RenderLayer& layer_, RenderSource* source_, uint32_t index_)
- : layer(layer_), source(source_), index(index_) {}
- std::reference_wrapper<RenderLayer> layer;
- RenderSource* source;
- uint32_t index;
- bool operator<(const RenderItem& other) const { return index < other.index; }
- };
-
- std::set<RenderItem> renderItems;
- std::vector<std::reference_wrapper<RenderLayer>> layersNeedPlacement;
- auto renderItemsEmplaceHint = renderItems.begin();
-
- // Update all sources and initialize renderItems.
- staticData->has3D = false;
- for (const auto& sourceImpl : *sourceImpls) {
- RenderSource* source = renderSources.at(sourceImpl->id).get();
- std::vector<Immutable<LayerProperties>> filteredLayersForSource;
- filteredLayersForSource.reserve(layerImpls->size());
- bool sourceNeedsRendering = false;
- bool sourceNeedsRelayout = false;
-
- uint32_t index = 0u;
- const auto begin = layerImpls->begin();
- const auto end = layerImpls->end();
- for (auto it = begin; it != end; ++it, ++index) {
- const Immutable<Layer::Impl>& layerImpl = *it;
- RenderLayer* layer = getRenderLayer(layerImpl->id);
- const auto* layerInfo = layerImpl->getTypeInfo();
- const bool layerNeedsRendering = layer->needsRendering();
- const bool zoomFitsLayer = layer->supportsZoom(zoomHistory.lastZoom);
- staticData->has3D = (staticData->has3D || layerInfo->pass3d == LayerTypeInfo::Pass3D::Required);
-
- if (layerInfo->source != LayerTypeInfo::Source::NotRequired) {
- if (layerImpl->source == sourceImpl->id) {
- sourceNeedsRelayout = (sourceNeedsRelayout || hasImageDiff || hasLayoutDifference(layerDiff, layerImpl->id));
- if (layerNeedsRendering) {
- filteredLayersForSource.push_back(layer->evaluatedProperties);
- if (zoomFitsLayer) {
- sourceNeedsRendering = true;
- renderItemsEmplaceHint = renderItems.emplace_hint(renderItemsEmplaceHint, *layer, source, index);
- }
- }
- }
- continue;
- }
-
- // Handle layers without source.
- if (layerNeedsRendering && zoomFitsLayer && sourceImpl.get() == sourceImpls->at(0).get()) {
- if (!backend.contextIsShared() && layerImpl.get() == layerImpls->at(0).get()) {
- const auto& solidBackground = layer->getSolidBackground();
- if (solidBackground) {
- backgroundColor = *solidBackground;
- continue; // This layer is shown with background color, and it shall not be added to render items.
- }
- }
- renderItemsEmplaceHint = renderItems.emplace_hint(renderItemsEmplaceHint, *layer, nullptr, index);
- }
- }
- source->update(sourceImpl,
- filteredLayersForSource,
- sourceNeedsRendering,
- sourceNeedsRelayout,
- tileParameters);
- }
-
- const bool loaded = updateParameters.styleLoaded && isLoaded();
- if (!isMapModeContinuous && !loaded) {
- return;
- }
-
+void Renderer::Impl::render(const RenderTree& renderTree) {
if (renderState == RenderState::Never) {
observer->onWillStartRenderingMap();
}
observer->onWillStartRenderingFrame();
+ const auto& renderTreeParameters = renderTree.getParameters();
- TransformParameters transformParams(updateParameters.transformState);
-
- // Update all matrices and generate data that we should upload to the GPU.
- for (const auto& entry : renderSources) {
- if (entry.second->isEnabled()) {
- entry.second->prepare({transformParams, updateParameters.debugOptions});
- }
- }
-
- for (auto& renderItem : renderItems) {
- RenderLayer& renderLayer = renderItem.layer;
- renderLayer.prepare({renderItem.source, *imageManager, *patternAtlas, updateParameters.transformState});
- if (renderLayer.needsPlacement()) {
- layersNeedPlacement.emplace_back(renderLayer);
- }
- }
-
- {
- if (!isMapModeContinuous) {
- // TODO: Think about right way for symbol index to handle still rendering
- crossTileSymbolIndex.reset();
- }
-
- bool symbolBucketsChanged = false;
- const bool placementChanged = !placement->stillRecent(updateParameters.timePoint);
- std::set<std::string> usedSymbolLayers;
- if (placementChanged) {
- placement = std::make_unique<Placement>(
- updateParameters.transformState, updateParameters.mode,
- updateParameters.transitionOptions, updateParameters.crossSourceCollisions,
- std::move(placement));
- }
-
- for (auto it = layersNeedPlacement.rbegin(); it != layersNeedPlacement.rend(); ++it) {
- const RenderLayer& layer = *it;
- if (crossTileSymbolIndex.addLayer(layer, updateParameters.transformState.getLatLng().longitude())) symbolBucketsChanged = true;
-
- if (placementChanged) {
- usedSymbolLayers.insert(layer.getID());
- placement->placeLayer(layer, transformParams.projMatrix, updateParameters.debugOptions & MapDebugOptions::Collision);
- }
- }
-
- if (placementChanged) {
- placement->commit(updateParameters.timePoint);
- crossTileSymbolIndex.pruneUnusedLayers(usedSymbolLayers);
- for (const auto& entry : renderSources) {
- entry.second->updateFadingTiles();
- }
- } else {
- placement->setStale();
- }
-
- for (auto it = layersNeedPlacement.rbegin(); it != layersNeedPlacement.rend(); ++it) {
- placement->updateLayerBuckets(*it, updateParameters.transformState, placementChanged || symbolBucketsChanged);
- }
+ if (!staticData) {
+ staticData = std::make_unique<RenderStaticData>(backend.getContext(), pixelRatio, programCacheDir);
}
+ staticData->has3D = renderTreeParameters.has3D;
auto& context = backend.getContext();
@@ -364,15 +66,19 @@ void Renderer::Impl::render(const UpdateParameters& updateParameters) {
context,
pixelRatio,
backend,
- updateParameters,
- renderLight.getEvaluated(),
- transformParams,
+ renderTreeParameters.light,
+ renderTreeParameters.mapMode,
+ renderTreeParameters.debugOptions,
+ renderTreeParameters.timePoint,
+ renderTreeParameters.transformParams,
*staticData,
- *lineAtlas,
- *patternAtlas
+ renderTree.getLineAtlas(),
+ renderTree.getPatternAtlas()
};
- parameters.symbolFadeChange = placement->symbolFadeChange(updateParameters.timePoint);
+ parameters.symbolFadeChange = renderTreeParameters.symbolFadeChange;
+ const auto& sourceRenderItems = renderTree.getSourceRenderItems();
+ const auto& layerRenderItems = renderTree.getLayerRenderItems();
// - UPLOAD PASS -------------------------------------------------------------------------------
// Uploads all required buffers and images before we do any actual rendering.
@@ -380,27 +86,15 @@ void Renderer::Impl::render(const UpdateParameters& updateParameters) {
const auto uploadPass = parameters.encoder->createUploadPass("upload");
// Update all clipping IDs + upload buckets.
- for (const auto& entry : renderSources) {
- if (entry.second->isEnabled()) {
- entry.second->upload(*uploadPass);
- }
+ for (const RenderItem& item : sourceRenderItems) {
+ item.upload(*uploadPass);
}
-
- UploadParameters uploadParameters{
- updateParameters.transformState,
- *lineAtlas,
- };
-
- for (auto& renderItem : renderItems) {
- RenderLayer& renderLayer = renderItem.layer;
- if (renderLayer.hasRenderPass(RenderPass::Upload)) {
- renderLayer.upload(*uploadPass, uploadParameters);
- }
+ for (const RenderItem& item : layerRenderItems) {
+ item.upload(*uploadPass);
}
-
staticData->upload(*uploadPass);
- lineAtlas->upload(*uploadPass);
- patternAtlas->upload(*uploadPass);
+ renderTree.getLineAtlas().upload(*uploadPass);
+ renderTree.getPatternAtlas().upload(*uploadPass);
}
// - 3D PASS -------------------------------------------------------------------------------------
@@ -419,13 +113,13 @@ void Renderer::Impl::render(const UpdateParameters& updateParameters) {
}
parameters.staticData.depthRenderbuffer->setShouldClear(true);
- uint32_t i = static_cast<uint32_t>(renderItems.size()) - 1;
- for (auto it = renderItems.begin(); it != renderItems.end(); ++it, --i) {
+ uint32_t i = static_cast<uint32_t>(layerRenderItems.size()) - 1;
+ for (auto it = layerRenderItems.begin(); it != layerRenderItems.end(); ++it, --i) {
parameters.currentLayer = i;
- RenderLayer& renderLayer = it->layer;
- if (renderLayer.hasRenderPass(parameters.pass)) {
- const auto layerDebugGroup(parameters.encoder->createDebugGroup(renderLayer.getID().c_str()));
- renderLayer.render(parameters);
+ const RenderItem& renderItem = it->get();
+ if (renderItem.hasRenderPass(parameters.pass)) {
+ const auto layerDebugGroup(parameters.encoder->createDebugGroup(renderItem.getName().c_str()));
+ renderItem.render(parameters);
}
}
}
@@ -438,14 +132,14 @@ void Renderer::Impl::render(const UpdateParameters& updateParameters) {
if (parameters.debugOptions & MapDebugOptions::Overdraw) {
color = Color::black();
} else if (!backend.contextIsShared()) {
- color = backgroundColor;
+ color = renderTreeParameters.backgroundColor;
}
parameters.renderPass = parameters.encoder->createRenderPass("main buffer", { parameters.backend.getDefaultRenderable(), color, 1, 0 });
}
// Actually render the layers
- parameters.depthRangeSize = 1 - (renderItems.size() + 2) * parameters.numSublayers * parameters.depthEpsilon;
+ parameters.depthRangeSize = 1 - (layerRenderItems.size() + 2) * parameters.numSublayers * parameters.depthEpsilon;
// - OPAQUE PASS -------------------------------------------------------------------------------
// Render everything top-to-bottom by using reverse iterators. Render opaque objects first.
@@ -454,12 +148,12 @@ void Renderer::Impl::render(const UpdateParameters& updateParameters) {
const auto debugGroup(parameters.renderPass->createDebugGroup("opaque"));
uint32_t i = 0;
- for (auto it = renderItems.rbegin(); it != renderItems.rend(); ++it, ++i) {
+ for (auto it = layerRenderItems.rbegin(); it != layerRenderItems.rend(); ++it, ++i) {
parameters.currentLayer = i;
- RenderLayer& renderLayer = it->layer;
- if (renderLayer.hasRenderPass(parameters.pass)) {
- const auto layerDebugGroup(parameters.renderPass->createDebugGroup(renderLayer.getID().c_str()));
- renderLayer.render(parameters);
+ const RenderItem& renderItem = it->get();
+ if (renderItem.hasRenderPass(parameters.pass)) {
+ const auto layerDebugGroup(parameters.renderPass->createDebugGroup(renderItem.getName().c_str()));
+ renderItem.render(parameters);
}
}
}
@@ -470,13 +164,13 @@ void Renderer::Impl::render(const UpdateParameters& updateParameters) {
parameters.pass = RenderPass::Translucent;
const auto debugGroup(parameters.renderPass->createDebugGroup("translucent"));
- uint32_t i = static_cast<uint32_t>(renderItems.size()) - 1;
- for (auto it = renderItems.begin(); it != renderItems.end(); ++it, --i) {
+ uint32_t i = static_cast<uint32_t>(layerRenderItems.size()) - 1;
+ for (auto it = layerRenderItems.begin(); it != layerRenderItems.end(); ++it, --i) {
parameters.currentLayer = i;
- RenderLayer& renderLayer = it->layer;
- if (renderLayer.hasRenderPass(parameters.pass)) {
- const auto layerDebugGroup(parameters.renderPass->createDebugGroup(renderLayer.getID().c_str()));
- renderLayer.render(parameters);
+ const RenderItem& renderItem = it->get();
+ if (renderItem.hasRenderPass(parameters.pass)) {
+ const auto layerDebugGroup(parameters.renderPass->createDebugGroup(renderItem.getName().c_str()));
+ renderItem.render(parameters);
}
}
}
@@ -490,10 +184,8 @@ void Renderer::Impl::render(const UpdateParameters& updateParameters) {
// This guarantees that we have at least one function per tile called.
// When only rendering layers via the stylesheet, it's possible that we don't
// ever visit a tile during rendering.
- for (const auto& entry : renderSources) {
- if (entry.second->isEnabled()) {
- entry.second->finishRender(parameters);
- }
+ for (const RenderItem& renderItem : sourceRenderItems) {
+ renderItem.render(parameters);
}
}
@@ -509,251 +201,30 @@ void Renderer::Impl::render(const UpdateParameters& updateParameters) {
// Ends the RenderPass
parameters.renderPass.reset();
-
- if (updateParameters.mode == MapMode::Continuous) {
+ const bool isMapModeContinuous = renderTreeParameters.mapMode == MapMode::Continuous;
+ if (isMapModeContinuous) {
parameters.encoder->present(parameters.backend.getDefaultRenderable());
}
// CommandEncoder destructor submits render commands.
parameters.encoder.reset();
-
- const bool needsRepaint = isMapModeContinuous && hasTransitions(parameters.timePoint);
observer->onDidFinishRenderingFrame(
- loaded ? RendererObserver::RenderMode::Full : RendererObserver::RenderMode::Partial,
- needsRepaint
+ renderTreeParameters.loaded ? RendererObserver::RenderMode::Full : RendererObserver::RenderMode::Partial,
+ renderTreeParameters.needsRepaint
);
- if (!loaded) {
+ if (!renderTreeParameters.loaded) {
renderState = RenderState::Partial;
} else if (renderState != RenderState::Fully) {
renderState = RenderState::Fully;
observer->onDidFinishRenderingMap();
- } else if (!needsRepaint) {
- // Notify observer about unused images when map is fully loaded
- // and there are no ongoing transitions.
- imageManager->reduceMemoryUseIfCacheSizeExceedsLimit();
- }
-}
-
-std::vector<Feature> Renderer::Impl::queryRenderedFeatures(const ScreenLineString& geometry, const RenderedQueryOptions& options) const {
- std::vector<const RenderLayer*> layers;
- if (options.layerIDs) {
- for (const auto& layerID : *options.layerIDs) {
- if (const RenderLayer* layer = getRenderLayer(layerID)) {
- layers.emplace_back(layer);
- }
- }
- } else {
- for (const auto& entry : renderLayers) {
- layers.emplace_back(entry.second.get());
- }
- }
-
- return queryRenderedFeatures(geometry, options, layers);
-}
-
-void Renderer::Impl::queryRenderedSymbols(std::unordered_map<std::string, std::vector<Feature>>& resultsByLayer,
- const ScreenLineString& geometry,
- const std::vector<const RenderLayer*>& layers,
- const RenderedQueryOptions& options) const {
-
- auto renderedSymbols = placement->getCollisionIndex().queryRenderedSymbols(geometry);
- std::vector<std::reference_wrapper<const RetainedQueryData>> bucketQueryData;
- for (auto entry : renderedSymbols) {
- bucketQueryData.emplace_back(placement->getQueryData(entry.first));
- }
- // Although symbol query is global, symbol results are only sortable within a bucket
- // For a predictable global sort renderItems, we sort the buckets based on their corresponding tile position
- std::sort(bucketQueryData.begin(), bucketQueryData.end(), [](const RetainedQueryData& a, const RetainedQueryData& b) {
- return
- std::tie(a.tileID.canonical.z, a.tileID.canonical.y, a.tileID.wrap, a.tileID.canonical.x) <
- std::tie(b.tileID.canonical.z, b.tileID.canonical.y, b.tileID.wrap, b.tileID.canonical.x);
- });
-
- for (auto wrappedQueryData : bucketQueryData) {
- auto& queryData = wrappedQueryData.get();
- auto bucketSymbols = queryData.featureIndex->lookupSymbolFeatures(renderedSymbols[queryData.bucketInstanceId],
- options,
- layers,
- queryData.tileID,
- queryData.featureSortOrder);
-
- for (auto layer : bucketSymbols) {
- auto& resultFeatures = resultsByLayer[layer.first];
- std::move(layer.second.begin(), layer.second.end(), std::inserter(resultFeatures, resultFeatures.end()));
- }
}
}
-std::vector<Feature> Renderer::Impl::queryRenderedFeatures(const ScreenLineString& geometry, const RenderedQueryOptions& options, const std::vector<const RenderLayer*>& layers) const {
- std::unordered_set<std::string> sourceIDs;
- for (const RenderLayer* layer : layers) {
- sourceIDs.emplace(layer->baseImpl->source);
- }
-
- mat4 projMatrix;
- transformState.getProjMatrix(projMatrix);
-
- std::unordered_map<std::string, std::vector<Feature>> resultsByLayer;
- for (const auto& sourceID : sourceIDs) {
- if (RenderSource* renderSource = getRenderSource(sourceID)) {
- auto sourceResults = renderSource->queryRenderedFeatures(geometry, transformState, layers, options, projMatrix);
- std::move(sourceResults.begin(), sourceResults.end(), std::inserter(resultsByLayer, resultsByLayer.begin()));
- }
- }
-
- queryRenderedSymbols(resultsByLayer, geometry, layers, options);
-
- std::vector<Feature> result;
-
- if (resultsByLayer.empty()) {
- return result;
- }
-
- // Combine all results based on the style layer renderItems.
- for (const auto& layerImpl : *layerImpls) {
- const RenderLayer* layer = getRenderLayer(layerImpl->id);
- if (!layer->needsRendering() || !layer->supportsZoom(zoomHistory.lastZoom)) {
- continue;
- }
-
- auto it = resultsByLayer.find(layer->baseImpl->id);
- if (it != resultsByLayer.end()) {
- std::move(it->second.begin(), it->second.end(), std::back_inserter(result));
- }
- }
-
- return result;
-}
-
-std::vector<Feature> Renderer::Impl::queryShapeAnnotations(const ScreenLineString& geometry) const {
- assert(LayerManager::annotationsEnabled);
- std::vector<const RenderLayer*> shapeAnnotationLayers;
- RenderedQueryOptions options;
- for (const auto& layerImpl : *layerImpls) {
- if (std::mismatch(layerImpl->id.begin(), layerImpl->id.end(),
- AnnotationManager::ShapeLayerID.begin(), AnnotationManager::ShapeLayerID.end()).second == AnnotationManager::ShapeLayerID.end()) {
- if (const RenderLayer* layer = getRenderLayer(layerImpl->id)) {
- shapeAnnotationLayers.emplace_back(layer);
- }
- }
- }
-
- return queryRenderedFeatures(geometry, options, shapeAnnotationLayers);
-}
-
-std::vector<Feature> Renderer::Impl::querySourceFeatures(const std::string& sourceID, const SourceQueryOptions& options) const {
- const RenderSource* source = getRenderSource(sourceID);
- if (!source) return {};
-
- return source->querySourceFeatures(options);
-}
-
-FeatureExtensionValue Renderer::Impl::queryFeatureExtensions(const std::string& sourceID,
- const Feature& feature,
- const std::string& extension,
- const std::string& extensionField,
- const optional<std::map<std::string, Value>>& args) const {
- if (RenderSource* renderSource = getRenderSource(sourceID)) {
- return renderSource->queryFeatureExtensions(feature, extension, extensionField, args);
- }
- return {};
-}
-
void Renderer::Impl::reduceMemoryUse() {
assert(gfx::BackendScope::exists());
- for (const auto& entry : renderSources) {
- entry.second->reduceMemoryUse();
- }
backend.getContext().reduceMemoryUsage();
- imageManager->reduceMemoryUse();
- observer->onInvalidate();
-}
-
-void Renderer::Impl::dumpDebugLogs() {
- for (const auto& entry : renderSources) {
- entry.second->dumpDebugLogs();
- }
-
- imageManager->dumpDebugLogs();
-}
-
-RenderLayer* Renderer::Impl::getRenderLayer(const std::string& id) {
- auto it = renderLayers.find(id);
- return it != renderLayers.end() ? it->second.get() : nullptr;
-}
-
-const RenderLayer* Renderer::Impl::getRenderLayer(const std::string& id) const {
- auto it = renderLayers.find(id);
- return it != renderLayers.end() ? it->second.get() : nullptr;
-}
-
-RenderSource* Renderer::Impl::getRenderSource(const std::string& id) const {
- auto it = renderSources.find(id);
- return it != renderSources.end() ? it->second.get() : nullptr;
-}
-
-bool Renderer::Impl::hasTransitions(TimePoint timePoint) const {
- if (renderLight.hasTransition()) {
- return true;
- }
-
- for (const auto& entry : renderLayers) {
- if (entry.second->hasTransition()) {
- return true;
- }
- }
-
- if (placement->hasTransitions(timePoint)) {
- return true;
- }
-
- for (const auto& entry : renderSources) {
- if (entry.second->hasFadingTiles()) {
- return true;
- }
- }
-
- return false;
-}
-
-bool Renderer::Impl::isLoaded() const {
- for (const auto& entry: renderSources) {
- if (!entry.second->isLoaded()) {
- return false;
- }
- }
-
- if (!imageManager->isLoaded()) {
- return false;
- }
-
- return true;
-}
-
-void Renderer::Impl::onGlyphsError(const FontStack& fontStack, const GlyphRange& glyphRange, std::exception_ptr error) {
- Log::Error(Event::Style, "Failed to load glyph range %d-%d for font stack %s: %s",
- glyphRange.first, glyphRange.second, fontStackToString(fontStack).c_str(), util::toString(error).c_str());
- observer->onResourceError(error);
-}
-
-void Renderer::Impl::onTileError(RenderSource& source, const OverscaledTileID& tileID, std::exception_ptr error) {
- Log::Error(Event::Style, "Failed to load tile %s for source %s: %s",
- util::toString(tileID).c_str(), source.baseImpl->id.c_str(), util::toString(error).c_str());
- observer->onResourceError(error);
-}
-
-void Renderer::Impl::onTileChanged(RenderSource&, const OverscaledTileID&) {
- observer->onInvalidate();
-}
-
-void Renderer::Impl::onStyleImageMissing(const std::string& id, std::function<void()> done) {
- observer->onStyleImageMissing(id, std::move(done));
-}
-
-void Renderer::Impl::onRemoveUnusedStyleImages(const std::vector<std::string>& unusedImageIDs) {
- observer->onRemoveUnusedStyleImages(unusedImageIDs);
}
} // namespace mbgl
diff --git a/src/mbgl/renderer/renderer_impl.hpp b/src/mbgl/renderer/renderer_impl.hpp
index fae79b6d52..85e37a014a 100644
--- a/src/mbgl/renderer/renderer_impl.hpp
+++ b/src/mbgl/renderer/renderer_impl.hpp
@@ -1,100 +1,39 @@
#pragma once
-#include <mbgl/renderer/renderer.hpp>
-#include <mbgl/renderer/render_source_observer.hpp>
-#include <mbgl/renderer/render_light.hpp>
-#include <mbgl/style/image.hpp>
-#include <mbgl/style/source.hpp>
-#include <mbgl/style/layer.hpp>
-#include <mbgl/map/transform_state.hpp>
-#include <mbgl/map/zoom_history.hpp>
-#include <mbgl/text/cross_tile_symbol_index.hpp>
-#include <mbgl/text/glyph_manager_observer.hpp>
-#include <mbgl/renderer/image_manager_observer.hpp>
-#include <mbgl/text/placement.hpp>
+#include <mbgl/renderer/render_orchestrator.hpp>
#include <memory>
#include <string>
-#include <vector>
namespace mbgl {
class RendererObserver;
-class RenderSource;
-class RenderLayer;
-class UpdateParameters;
class RenderStaticData;
-class RenderedQueryOptions;
-class SourceQueryOptions;
-class GlyphManager;
-class ImageManager;
-class LineAtlas;
-class PatternAtlas;
-class CrossTileSymbolIndex;
+class RenderTree;
namespace gfx {
class RendererBackend;
} // namespace gfx
-class Renderer::Impl : public GlyphManagerObserver,
- public ImageManagerObserver,
- public RenderSourceObserver{
+class Renderer::Impl {
public:
Impl(gfx::RendererBackend&,
float pixelRatio_,
- const optional<std::string> programCacheDir,
- const optional<std::string> localFontFamily_);
- ~Impl() final;
+ optional<std::string> programCacheDir,
+ optional<std::string> localFontFamily_);
+ ~Impl();
- void markContextLost() {
- contextLost = true;
- };
+private:
+ friend class Renderer;
void setObserver(RendererObserver*);
- void render(const UpdateParameters&);
-
- std::vector<Feature> queryRenderedFeatures(const ScreenLineString&, const RenderedQueryOptions&) const;
- std::vector<Feature> querySourceFeatures(const std::string& sourceID, const SourceQueryOptions&) const;
- std::vector<Feature> queryShapeAnnotations(const ScreenLineString&) const;
-
- FeatureExtensionValue queryFeatureExtensions(const std::string& sourceID,
- const Feature& feature,
- const std::string& extension,
- const std::string& extensionField,
- const optional<std::map<std::string, Value>>& args) const;
+ void render(const RenderTree&);
void reduceMemoryUse();
- void dumpDebugLogs();
-
-private:
- bool isLoaded() const;
- bool hasTransitions(TimePoint) const;
-
- RenderSource* getRenderSource(const std::string& id) const;
-
- RenderLayer* getRenderLayer(const std::string& id);
- const RenderLayer* getRenderLayer(const std::string& id) const;
-
- void queryRenderedSymbols(std::unordered_map<std::string, std::vector<Feature>>& resultsByLayer,
- const ScreenLineString& geometry,
- const std::vector<const RenderLayer*>& layers,
- const RenderedQueryOptions& options) const;
-
- std::vector<Feature> queryRenderedFeatures(const ScreenLineString&, const RenderedQueryOptions&, const std::vector<const RenderLayer*>&) const;
-
- // GlyphManagerObserver implementation.
- void onGlyphsError(const FontStack&, const GlyphRange&, std::exception_ptr) override;
- // RenderSourceObserver implementation.
- void onTileChanged(RenderSource&, const OverscaledTileID&) override;
- void onTileError(RenderSource&, const OverscaledTileID&, std::exception_ptr) override;
-
- // ImageManagerObserver implementation
- void onStyleImageMissing(const std::string&, std::function<void()>) override;
- void onRemoveUnusedStyleImages(const std::vector<std::string>&) final;
-
- friend class Renderer;
+ // TODO: Move orchestrator to Map::Impl.
+ RenderOrchestrator orchestrator;
gfx::RendererBackend& backend;
@@ -102,7 +41,7 @@ private:
const float pixelRatio;
const optional<std::string> programCacheDir;
- const optional<std::string> localFontFamily;
+ std::unique_ptr<RenderStaticData> staticData;
enum class RenderState {
Never,
@@ -111,27 +50,6 @@ private:
};
RenderState renderState = RenderState::Never;
- ZoomHistory zoomHistory;
- TransformState transformState;
-
- std::unique_ptr<GlyphManager> glyphManager;
- std::unique_ptr<ImageManager> imageManager;
- std::unique_ptr<LineAtlas> lineAtlas;
- std::unique_ptr<PatternAtlas> patternAtlas;
- std::unique_ptr<RenderStaticData> staticData;
-
- Immutable<std::vector<Immutable<style::Image::Impl>>> imageImpls;
- Immutable<std::vector<Immutable<style::Source::Impl>>> sourceImpls;
- Immutable<std::vector<Immutable<style::Layer::Impl>>> layerImpls;
-
- std::unordered_map<std::string, std::unique_ptr<RenderSource>> renderSources;
- std::unordered_map<std::string, std::unique_ptr<RenderLayer>> renderLayers;
- RenderLight renderLight;
-
- CrossTileSymbolIndex crossTileSymbolIndex;
- std::unique_ptr<Placement> placement;
-
- bool contextLost = false;
};
} // namespace mbgl
diff --git a/src/mbgl/renderer/tile_pyramid.cpp b/src/mbgl/renderer/tile_pyramid.cpp
index dee84c4e32..b931b2d279 100644
--- a/src/mbgl/renderer/tile_pyramid.cpp
+++ b/src/mbgl/renderer/tile_pyramid.cpp
@@ -231,12 +231,7 @@ void TilePyramid::update(const std::vector<Immutable<style::LayerProperties>>& l
if (holdForFade && typeInfo->fadingTiles == LayerTypeInfo::FadingTiles::NotRequired) {
continue;
}
- // Update layer properties for complete tiles; for incomplete just check the presence.
- bool layerRenderableInTile = tile.isComplete() ? tile.updateLayerProperties(layerProperties)
- : static_cast<bool>(tile.getBucket(*layerProperties->baseImpl));
- if (layerRenderableInTile) {
- tile.usedByRenderedLayers = true;
- }
+ tile.usedByRenderedLayers |= tile.layerPropertiesUpdated(layerProperties);
}
}
}
diff --git a/src/mbgl/storage/resource_options.cpp b/src/mbgl/storage/resource_options.cpp
index c56a22540b..21ecca979a 100644
--- a/src/mbgl/storage/resource_options.cpp
+++ b/src/mbgl/storage/resource_options.cpp
@@ -10,6 +10,7 @@ public:
std::string cachePath = ":memory:";
std::string assetPath = ".";
uint64_t maximumSize = mbgl::util::DEFAULT_MAX_CACHE_SIZE;
+ bool supportCacheOnlyRequests = true;
void* platformContext = nullptr;
};
@@ -68,6 +69,15 @@ uint64_t ResourceOptions::maximumCacheSize() const {
return impl_->maximumSize;
}
+ResourceOptions& ResourceOptions::withCacheOnlyRequestsSupport(bool supportCacheOnlyRequests) {
+ impl_->supportCacheOnlyRequests = supportCacheOnlyRequests;
+ return *this;
+}
+
+bool ResourceOptions::supportsCacheOnlyRequests() const {
+ return impl_->supportCacheOnlyRequests;
+}
+
ResourceOptions& ResourceOptions::withPlatformContext(void* context) {
impl_->platformContext = context;
return *this;
diff --git a/src/mbgl/style/expression/coercion.cpp b/src/mbgl/style/expression/coercion.cpp
index e0c29be1a2..284d6f2e3e 100644
--- a/src/mbgl/style/expression/coercion.cpp
+++ b/src/mbgl/style/expression/coercion.cpp
@@ -25,7 +25,7 @@ EvaluationResult toNumber(const Value& v) {
[](const std::string& s) -> optional<double> {
try {
return util::stof(s);
- } catch (const std::exception&) {
+ } catch (...) {
return optional<double>();
}
},
diff --git a/src/mbgl/style/expression/number_format.cpp b/src/mbgl/style/expression/number_format.cpp
new file mode 100644
index 0000000000..e31a9ce398
--- /dev/null
+++ b/src/mbgl/style/expression/number_format.cpp
@@ -0,0 +1,215 @@
+#include <mbgl/style/expression/number_format.hpp>
+#include <mbgl/style/conversion_impl.hpp>
+#include <mbgl/util/platform.hpp>
+
+namespace mbgl {
+namespace style {
+namespace expression {
+
+const char localeKey[] = "locale";
+const char currencyKey[] = "currency";
+const char minFractionDigitsKey[] = "min-fraction-digits";
+const char maxFractionDigitsKey[] = "max-fraction-digits";
+
+NumberFormat::NumberFormat(std::unique_ptr<Expression> number_,
+ std::unique_ptr<Expression> locale_,
+ std::unique_ptr<Expression> currency_,
+ std::unique_ptr<Expression> minFractionDigits_,
+ std::unique_ptr<Expression> maxFractionDigits_)
+ : Expression(Kind::NumberFormat, type::String),
+ number(std::move(number_)),
+ locale(std::move(locale_)),
+ currency(std::move(currency_)),
+ minFractionDigits(std::move(minFractionDigits_)),
+ maxFractionDigits(std::move(maxFractionDigits_))
+{}
+
+NumberFormat::~NumberFormat() = default;
+
+EvaluationResult NumberFormat::evaluate(const EvaluationContext& params) const {
+ auto numberResult = number->evaluate(params);
+ if (!numberResult) {
+ return numberResult.error();
+ }
+ double evaluatedNumber = numberResult->get<double>();
+
+ std::string evaluatedLocale;
+ if (locale) {
+ auto localeResult = locale->evaluate(params);
+ if (!localeResult) {
+ return localeResult.error();
+ }
+ evaluatedLocale = toString(*localeResult);
+ }
+
+ std::string evaluatedCurrency;
+ if (currency) {
+ auto currencyResult = currency->evaluate(params);
+ if (!currencyResult) {
+ return currencyResult.error();
+ }
+ evaluatedCurrency = toString(*currencyResult);
+ }
+
+ uint8_t evaluatedMinFractionDigits = 0;
+ if (minFractionDigits) {
+ auto minDigitsResult = minFractionDigits->evaluate(params);
+ if (!minDigitsResult) {
+ return minDigitsResult.error();
+ }
+ evaluatedMinFractionDigits = minDigitsResult->get<double>();
+ }
+
+ uint8_t evaluatedMaxFractionDigits = 3;
+ if (maxFractionDigits) {
+ auto maxDigitsResult = maxFractionDigits->evaluate(params);
+ if (!maxDigitsResult) {
+ return maxDigitsResult.error();
+ }
+ evaluatedMaxFractionDigits = maxDigitsResult->get<double>();
+ }
+
+ std::string output;
+ output = platform::formatNumber(evaluatedNumber,
+ evaluatedLocale,
+ evaluatedCurrency,
+ evaluatedMinFractionDigits,
+ evaluatedMaxFractionDigits);
+ return output;
+}
+
+void NumberFormat::eachChild(const std::function<void(const Expression&)>& visit) const {
+ visit(*number);
+ if (locale) visit(*locale);
+ if (currency) visit(*currency);
+ if (minFractionDigits) visit(*minFractionDigits);
+ if (maxFractionDigits) visit(*maxFractionDigits);
+}
+
+bool NumberFormat::operator==(const Expression& e) const {
+ if (e.getKind() == Kind::NumberFormat) {
+ auto rhs = static_cast<const NumberFormat*>(&e);
+ if ((locale && (!rhs->locale || *locale != *rhs->locale)) ||
+ (!locale && rhs->locale)) {
+ return false;
+ }
+ if ((currency && (!rhs->currency || *currency != *rhs->currency)) ||
+ (!currency && rhs->currency)) {
+ return false;
+ }
+ if ((minFractionDigits && (!rhs->minFractionDigits || *minFractionDigits != *rhs->minFractionDigits)) ||
+ (!minFractionDigits && rhs->minFractionDigits)) {
+ return false;
+ }
+ if ((maxFractionDigits && (!rhs->maxFractionDigits || *maxFractionDigits != *rhs->maxFractionDigits)) ||
+ (!maxFractionDigits && rhs->maxFractionDigits)) {
+ return false;
+ }
+ return *number == *rhs->number;
+ }
+ return false;
+}
+
+std::vector<optional<Value>> NumberFormat::possibleOutputs() const {
+ return { nullopt };
+}
+
+using namespace mbgl::style::conversion;
+ParseResult NumberFormat::parse(const Convertible& value, ParsingContext& ctx) {
+ std::size_t length = arrayLength(value);
+
+ if (length != 3) {
+ ctx.error("Expected two arguments, but found " + util::toString(length) + " instead.");
+ return ParseResult();
+ }
+
+ ParseResult numberResult = ctx.parse(arrayMember(value, 1), 1, {type::Number});
+ if (!numberResult) {
+ ctx.error("Failed to parse the number.");
+ return ParseResult();
+ }
+
+ type::Type type = (*numberResult)->getType();
+ if (!type.is<type::NumberType>()) {
+ ctx.error("Expected argument of type number, but found " + toString(type) + " instead.");
+ return ParseResult();
+ }
+
+ auto options = arrayMember(value, 2);
+ if (!isObject(options)) {
+ ctx.error("Number-format options argument must be an object.");
+ return ParseResult();
+ }
+
+ const optional<Convertible> localeOption = objectMember(options, localeKey);
+ ParseResult localeResult;
+ if (localeOption) {
+ localeResult = ctx.parse(*localeOption, 1, {type::String});
+ if (!localeResult) {
+ ctx.error("Number-format locale parsing failed.");
+ return ParseResult();
+ }
+ }
+
+ const optional<Convertible> currencyOption = objectMember(options, currencyKey);
+ ParseResult currencyResult;
+ if (currencyOption) {
+ currencyResult = ctx.parse(*currencyOption, 1, {type::String});
+ if (!currencyResult) {
+ ctx.error("Number-format currency parsing failed.");
+ return ParseResult();
+ }
+ }
+
+ const optional<Convertible> minFractionDigitsOption = objectMember(options, minFractionDigitsKey);
+ ParseResult minFractionDigitsResult;
+ if (minFractionDigitsOption) {
+ minFractionDigitsResult = ctx.parse(*minFractionDigitsOption, 1, {type::Number});
+ if (!minFractionDigitsResult) {
+ ctx.error("Number-format min-fraction-digits parsing failed.");
+ return ParseResult();
+ }
+ }
+
+ const optional<Convertible> maxFractionDigitsOption = objectMember(options, maxFractionDigitsKey);
+ ParseResult maxFractionDigitsResult;
+ if (maxFractionDigitsOption) {
+ maxFractionDigitsResult = ctx.parse(*maxFractionDigitsOption, 1, {type::Number});
+ if (!maxFractionDigitsResult) {
+ ctx.error("Number-format max-fraction-digits parsing failed.");
+ return ParseResult();
+ }
+ }
+
+ return ParseResult(std::make_unique<NumberFormat>(std::move(*numberResult),
+ localeResult ? std::move(*localeResult) : nullptr,
+ currencyResult? std::move(*currencyResult) : nullptr,
+ minFractionDigitsResult ? std::move(*minFractionDigitsResult) : nullptr,
+ maxFractionDigitsResult ? std::move(*maxFractionDigitsResult) : nullptr));
+}
+
+mbgl::Value NumberFormat::serialize() const {
+ std::vector<mbgl::Value> serialized{{ getOperator() }};
+ serialized.emplace_back(number->serialize());
+
+ std::unordered_map<std::string, mbgl::Value> options;
+ if (locale) {
+ options[localeKey] = locale->serialize();
+ }
+ if (currency) {
+ options[currencyKey] = currency->serialize();
+ }
+ if (minFractionDigits) {
+ options[minFractionDigitsKey] = minFractionDigits->serialize();
+ }
+ if (maxFractionDigits) {
+ options[maxFractionDigitsKey] = maxFractionDigits->serialize();
+ }
+ serialized.emplace_back(options);
+
+ return serialized;
+}
+
+} // namespace expression
+} // namespace style
+} // namespace mbgl
diff --git a/src/mbgl/style/expression/parsing_context.cpp b/src/mbgl/style/expression/parsing_context.cpp
index 411aa660e8..a7c04f563d 100644
--- a/src/mbgl/style/expression/parsing_context.cpp
+++ b/src/mbgl/style/expression/parsing_context.cpp
@@ -13,6 +13,7 @@
#include <mbgl/style/expression/coercion.hpp>
#include <mbgl/style/expression/compound_expression.hpp>
#include <mbgl/style/expression/comparison.hpp>
+#include <mbgl/style/expression/number_format.hpp>
#include <mbgl/style/expression/format_expression.hpp>
#include <mbgl/style/expression/interpolate.hpp>
#include <mbgl/style/expression/length.hpp>
@@ -120,6 +121,7 @@ MAPBOX_ETERNAL_CONSTEXPR const auto expressionRegistry = mapbox::eternal::hash_m
{"literal", Literal::parse},
{"match", parseMatch},
{"number", Assertion::parse},
+ {"number-format", NumberFormat::parse},
{"object", Assertion::parse},
{"step", Step::parse},
{"string", Assertion::parse},
diff --git a/src/mbgl/style/layers/background_layer_properties.cpp b/src/mbgl/style/layers/background_layer_properties.cpp
index f0da811379..63f6a7be68 100644
--- a/src/mbgl/style/layers/background_layer_properties.cpp
+++ b/src/mbgl/style/layers/background_layer_properties.cpp
@@ -21,6 +21,10 @@ BackgroundLayerProperties::BackgroundLayerProperties(
BackgroundLayerProperties::~BackgroundLayerProperties() = default;
+unsigned long BackgroundLayerProperties::constantsMask() const {
+ return evaluated.constantsMask();
+}
+
const BackgroundLayer::Impl& BackgroundLayerProperties::layerImpl() const {
return static_cast<const BackgroundLayer::Impl&>(*baseImpl);
}
diff --git a/src/mbgl/style/layers/background_layer_properties.hpp b/src/mbgl/style/layers/background_layer_properties.hpp
index 371e8639f1..56db696533 100644
--- a/src/mbgl/style/layers/background_layer_properties.hpp
+++ b/src/mbgl/style/layers/background_layer_properties.hpp
@@ -41,6 +41,8 @@ public:
BackgroundPaintProperties::PossiblyEvaluated);
~BackgroundLayerProperties() override;
+ unsigned long constantsMask() const override;
+
const BackgroundLayer::Impl& layerImpl() const;
// Data members.
CrossfadeParameters crossfade;
diff --git a/src/mbgl/style/layers/circle_layer_properties.cpp b/src/mbgl/style/layers/circle_layer_properties.cpp
index fbb4aeabd4..03a4a8f56e 100644
--- a/src/mbgl/style/layers/circle_layer_properties.cpp
+++ b/src/mbgl/style/layers/circle_layer_properties.cpp
@@ -19,6 +19,10 @@ CircleLayerProperties::CircleLayerProperties(
CircleLayerProperties::~CircleLayerProperties() = default;
+unsigned long CircleLayerProperties::constantsMask() const {
+ return evaluated.constantsMask();
+}
+
const CircleLayer::Impl& CircleLayerProperties::layerImpl() const {
return static_cast<const CircleLayer::Impl&>(*baseImpl);
}
diff --git a/src/mbgl/style/layers/circle_layer_properties.hpp b/src/mbgl/style/layers/circle_layer_properties.hpp
index 17727c79eb..1aaa3bb3e3 100644
--- a/src/mbgl/style/layers/circle_layer_properties.hpp
+++ b/src/mbgl/style/layers/circle_layer_properties.hpp
@@ -80,6 +80,8 @@ public:
CirclePaintProperties::PossiblyEvaluated);
~CircleLayerProperties() override;
+ unsigned long constantsMask() const override;
+
const CircleLayer::Impl& layerImpl() const;
// Data members.
CirclePaintProperties::PossiblyEvaluated evaluated;
diff --git a/src/mbgl/style/layers/fill_extrusion_layer_properties.cpp b/src/mbgl/style/layers/fill_extrusion_layer_properties.cpp
index 7986c40923..ddfe9c8468 100644
--- a/src/mbgl/style/layers/fill_extrusion_layer_properties.cpp
+++ b/src/mbgl/style/layers/fill_extrusion_layer_properties.cpp
@@ -21,6 +21,10 @@ FillExtrusionLayerProperties::FillExtrusionLayerProperties(
FillExtrusionLayerProperties::~FillExtrusionLayerProperties() = default;
+unsigned long FillExtrusionLayerProperties::constantsMask() const {
+ return evaluated.constantsMask();
+}
+
const FillExtrusionLayer::Impl& FillExtrusionLayerProperties::layerImpl() const {
return static_cast<const FillExtrusionLayer::Impl&>(*baseImpl);
}
diff --git a/src/mbgl/style/layers/fill_extrusion_layer_properties.hpp b/src/mbgl/style/layers/fill_extrusion_layer_properties.hpp
index f5aeaf5c73..69b8a0bcb8 100644
--- a/src/mbgl/style/layers/fill_extrusion_layer_properties.hpp
+++ b/src/mbgl/style/layers/fill_extrusion_layer_properties.hpp
@@ -66,6 +66,8 @@ public:
FillExtrusionPaintProperties::PossiblyEvaluated);
~FillExtrusionLayerProperties() override;
+ unsigned long constantsMask() const override;
+
const FillExtrusionLayer::Impl& layerImpl() const;
// Data members.
CrossfadeParameters crossfade;
diff --git a/src/mbgl/style/layers/fill_layer_properties.cpp b/src/mbgl/style/layers/fill_layer_properties.cpp
index 55e5a58cce..de917177f6 100644
--- a/src/mbgl/style/layers/fill_layer_properties.cpp
+++ b/src/mbgl/style/layers/fill_layer_properties.cpp
@@ -21,6 +21,10 @@ FillLayerProperties::FillLayerProperties(
FillLayerProperties::~FillLayerProperties() = default;
+unsigned long FillLayerProperties::constantsMask() const {
+ return evaluated.constantsMask();
+}
+
const FillLayer::Impl& FillLayerProperties::layerImpl() const {
return static_cast<const FillLayer::Impl&>(*baseImpl);
}
diff --git a/src/mbgl/style/layers/fill_layer_properties.hpp b/src/mbgl/style/layers/fill_layer_properties.hpp
index 376852d254..4e1b6970fe 100644
--- a/src/mbgl/style/layers/fill_layer_properties.hpp
+++ b/src/mbgl/style/layers/fill_layer_properties.hpp
@@ -61,6 +61,8 @@ public:
FillPaintProperties::PossiblyEvaluated);
~FillLayerProperties() override;
+ unsigned long constantsMask() const override;
+
const FillLayer::Impl& layerImpl() const;
// Data members.
CrossfadeParameters crossfade;
diff --git a/src/mbgl/style/layers/heatmap_layer_properties.cpp b/src/mbgl/style/layers/heatmap_layer_properties.cpp
index 03a73be103..cc50073651 100644
--- a/src/mbgl/style/layers/heatmap_layer_properties.cpp
+++ b/src/mbgl/style/layers/heatmap_layer_properties.cpp
@@ -19,6 +19,10 @@ HeatmapLayerProperties::HeatmapLayerProperties(
HeatmapLayerProperties::~HeatmapLayerProperties() = default;
+unsigned long HeatmapLayerProperties::constantsMask() const {
+ return evaluated.constantsMask();
+}
+
const HeatmapLayer::Impl& HeatmapLayerProperties::layerImpl() const {
return static_cast<const HeatmapLayer::Impl&>(*baseImpl);
}
diff --git a/src/mbgl/style/layers/heatmap_layer_properties.hpp b/src/mbgl/style/layers/heatmap_layer_properties.hpp
index dda9808e77..634cbef1ed 100644
--- a/src/mbgl/style/layers/heatmap_layer_properties.hpp
+++ b/src/mbgl/style/layers/heatmap_layer_properties.hpp
@@ -49,6 +49,8 @@ public:
HeatmapPaintProperties::PossiblyEvaluated);
~HeatmapLayerProperties() override;
+ unsigned long constantsMask() const override;
+
const HeatmapLayer::Impl& layerImpl() const;
// Data members.
HeatmapPaintProperties::PossiblyEvaluated evaluated;
diff --git a/src/mbgl/style/layers/hillshade_layer_properties.cpp b/src/mbgl/style/layers/hillshade_layer_properties.cpp
index 407acb6fc0..72acc235e2 100644
--- a/src/mbgl/style/layers/hillshade_layer_properties.cpp
+++ b/src/mbgl/style/layers/hillshade_layer_properties.cpp
@@ -19,6 +19,10 @@ HillshadeLayerProperties::HillshadeLayerProperties(
HillshadeLayerProperties::~HillshadeLayerProperties() = default;
+unsigned long HillshadeLayerProperties::constantsMask() const {
+ return evaluated.constantsMask();
+}
+
const HillshadeLayer::Impl& HillshadeLayerProperties::layerImpl() const {
return static_cast<const HillshadeLayer::Impl&>(*baseImpl);
}
diff --git a/src/mbgl/style/layers/hillshade_layer_properties.hpp b/src/mbgl/style/layers/hillshade_layer_properties.hpp
index 85ef8b967c..e38e5bb5b3 100644
--- a/src/mbgl/style/layers/hillshade_layer_properties.hpp
+++ b/src/mbgl/style/layers/hillshade_layer_properties.hpp
@@ -55,6 +55,8 @@ public:
HillshadePaintProperties::PossiblyEvaluated);
~HillshadeLayerProperties() override;
+ unsigned long constantsMask() const override;
+
const HillshadeLayer::Impl& layerImpl() const;
// Data members.
HillshadePaintProperties::PossiblyEvaluated evaluated;
diff --git a/src/mbgl/style/layers/layer_properties.cpp.ejs b/src/mbgl/style/layers/layer_properties.cpp.ejs
index 2c8e1ce9a0..fcdcfda7be 100644
--- a/src/mbgl/style/layers/layer_properties.cpp.ejs
+++ b/src/mbgl/style/layers/layer_properties.cpp.ejs
@@ -30,6 +30,10 @@ namespace style {
<%- camelize(type) %>LayerProperties::~<%- camelize(type) %>LayerProperties() = default;
+unsigned long <%- camelize(type) %>LayerProperties::constantsMask() const {
+ return evaluated.constantsMask();
+}
+
const <%- camelize(type) %>Layer::Impl& <%- camelize(type) %>LayerProperties::layerImpl() const {
return static_cast<const <%- camelize(type) %>Layer::Impl&>(*baseImpl);
}
diff --git a/src/mbgl/style/layers/layer_properties.hpp.ejs b/src/mbgl/style/layers/layer_properties.hpp.ejs
index 277cfd173b..4513146d6d 100644
--- a/src/mbgl/style/layers/layer_properties.hpp.ejs
+++ b/src/mbgl/style/layers/layer_properties.hpp.ejs
@@ -72,6 +72,8 @@ public:
<%- camelize(type) %>PaintProperties::PossiblyEvaluated);
~<%- camelize(type) %>LayerProperties() override;
+ unsigned long constantsMask() const override;
+
const <%- camelize(type) %>Layer::Impl& layerImpl() const;
// Data members.
<% if (type === 'background' || type === 'fill' || type === 'line' || type === 'fill-extrusion') { -%>
diff --git a/src/mbgl/style/layers/line_layer_properties.cpp b/src/mbgl/style/layers/line_layer_properties.cpp
index 09fb6ff80c..46b220661c 100644
--- a/src/mbgl/style/layers/line_layer_properties.cpp
+++ b/src/mbgl/style/layers/line_layer_properties.cpp
@@ -21,6 +21,10 @@ LineLayerProperties::LineLayerProperties(
LineLayerProperties::~LineLayerProperties() = default;
+unsigned long LineLayerProperties::constantsMask() const {
+ return evaluated.constantsMask();
+}
+
const LineLayer::Impl& LineLayerProperties::layerImpl() const {
return static_cast<const LineLayer::Impl&>(*baseImpl);
}
diff --git a/src/mbgl/style/layers/line_layer_properties.hpp b/src/mbgl/style/layers/line_layer_properties.hpp
index 7463d94393..dac6cebae0 100644
--- a/src/mbgl/style/layers/line_layer_properties.hpp
+++ b/src/mbgl/style/layers/line_layer_properties.hpp
@@ -113,6 +113,8 @@ public:
LinePaintProperties::PossiblyEvaluated);
~LineLayerProperties() override;
+ unsigned long constantsMask() const override;
+
const LineLayer::Impl& layerImpl() const;
// Data members.
CrossfadeParameters crossfade;
diff --git a/src/mbgl/style/layers/raster_layer_properties.cpp b/src/mbgl/style/layers/raster_layer_properties.cpp
index 4d9245a218..77bdb62b8d 100644
--- a/src/mbgl/style/layers/raster_layer_properties.cpp
+++ b/src/mbgl/style/layers/raster_layer_properties.cpp
@@ -19,6 +19,10 @@ RasterLayerProperties::RasterLayerProperties(
RasterLayerProperties::~RasterLayerProperties() = default;
+unsigned long RasterLayerProperties::constantsMask() const {
+ return evaluated.constantsMask();
+}
+
const RasterLayer::Impl& RasterLayerProperties::layerImpl() const {
return static_cast<const RasterLayer::Impl&>(*baseImpl);
}
diff --git a/src/mbgl/style/layers/raster_layer_properties.hpp b/src/mbgl/style/layers/raster_layer_properties.hpp
index 61aa32de2d..9ddd6cc790 100644
--- a/src/mbgl/style/layers/raster_layer_properties.hpp
+++ b/src/mbgl/style/layers/raster_layer_properties.hpp
@@ -65,6 +65,8 @@ public:
RasterPaintProperties::PossiblyEvaluated);
~RasterLayerProperties() override;
+ unsigned long constantsMask() const override;
+
const RasterLayer::Impl& layerImpl() const;
// Data members.
RasterPaintProperties::PossiblyEvaluated evaluated;
diff --git a/src/mbgl/style/layers/symbol_layer_properties.cpp b/src/mbgl/style/layers/symbol_layer_properties.cpp
index aeca39b419..5e50535377 100644
--- a/src/mbgl/style/layers/symbol_layer_properties.cpp
+++ b/src/mbgl/style/layers/symbol_layer_properties.cpp
@@ -19,6 +19,10 @@ SymbolLayerProperties::SymbolLayerProperties(
SymbolLayerProperties::~SymbolLayerProperties() = default;
+unsigned long SymbolLayerProperties::constantsMask() const {
+ return evaluated.constantsMask();
+}
+
const SymbolLayer::Impl& SymbolLayerProperties::layerImpl() const {
return static_cast<const SymbolLayer::Impl&>(*baseImpl);
}
diff --git a/src/mbgl/style/layers/symbol_layer_properties.hpp b/src/mbgl/style/layers/symbol_layer_properties.hpp
index 7b630a1ca3..0c2bcd2661 100644
--- a/src/mbgl/style/layers/symbol_layer_properties.hpp
+++ b/src/mbgl/style/layers/symbol_layer_properties.hpp
@@ -341,6 +341,8 @@ public:
SymbolPaintProperties::PossiblyEvaluated);
~SymbolLayerProperties() override;
+ unsigned long constantsMask() const override;
+
const SymbolLayer::Impl& layerImpl() const;
// Data members.
SymbolPaintProperties::PossiblyEvaluated evaluated;
diff --git a/src/mbgl/style/properties.hpp b/src/mbgl/style/properties.hpp
index 7f58ff223d..9d66f850de 100644
--- a/src/mbgl/style/properties.hpp
+++ b/src/mbgl/style/properties.hpp
@@ -9,6 +9,8 @@
#include <mbgl/util/indexed_tuple.hpp>
#include <mbgl/util/ignore.hpp>
+#include <bitset>
+
namespace mbgl {
class GeometryTileFeature;
@@ -102,6 +104,22 @@ struct IsDataDriven : std::integral_constant<bool, P::IsDataDriven> {};
template <class P>
struct IsOverridable : std::integral_constant<bool, P::IsOverridable> {};
+template <class Ps>
+struct ConstantsMask;
+
+template <class... Ps>
+struct ConstantsMask<TypeList<Ps...>> {
+ template <class Properties>
+ static unsigned long getMask(const Properties& properties) {
+ std::bitset<sizeof... (Ps)> result;
+ util::ignore({
+ result.set(TypeIndex<Ps, Ps...>::value,
+ properties.template get<Ps>().isConstant())...
+ });
+ return result.to_ulong();
+ }
+};
+
template <class... Ps>
class Properties {
public:
@@ -172,6 +190,10 @@ public:
evaluate<Ps>(z, feature)...
};
}
+
+ unsigned long constantsMask() const {
+ return ConstantsMask<DataDrivenProperties>::getMask(*this);
+ }
};
class Unevaluated : public Tuple<UnevaluatedTypes> {
diff --git a/src/mbgl/tile/geometry_tile.cpp b/src/mbgl/tile/geometry_tile.cpp
index 7a7b7cb770..1bc7646d1e 100644
--- a/src/mbgl/tile/geometry_tile.cpp
+++ b/src/mbgl/tile/geometry_tile.cpp
@@ -212,13 +212,14 @@ const LayerRenderData* GeometryTile::getLayerRenderData(const style::Layer::Impl
return that->getMutableLayerRenderData(layerImpl);
}
-bool GeometryTile::updateLayerProperties(const Immutable<style::LayerProperties>& layerProperties) {
+bool GeometryTile::layerPropertiesUpdated(const Immutable<style::LayerProperties>& layerProperties) {
LayerRenderData* renderData = getMutableLayerRenderData(*layerProperties->baseImpl);
if (!renderData) {
return false;
}
- if (renderData->layerProperties != layerProperties) {
+ if (renderData->layerProperties != layerProperties &&
+ renderData->layerProperties->constantsMask() == layerProperties->constantsMask()) {
renderData->layerProperties = layerProperties;
}
diff --git a/src/mbgl/tile/geometry_tile.hpp b/src/mbgl/tile/geometry_tile.hpp
index cbc94deed0..c206dd1157 100644
--- a/src/mbgl/tile/geometry_tile.hpp
+++ b/src/mbgl/tile/geometry_tile.hpp
@@ -47,7 +47,7 @@ public:
void upload(gfx::UploadPass&) override;
Bucket* getBucket(const style::Layer::Impl&) const override;
const LayerRenderData* getLayerRenderData(const style::Layer::Impl&) const override;
- bool updateLayerProperties(const Immutable<style::LayerProperties>&) override;
+ bool layerPropertiesUpdated(const Immutable<style::LayerProperties>&) override;
void queryRenderedFeatures(
std::unordered_map<std::string, std::vector<Feature>>& result,
diff --git a/src/mbgl/tile/tile.hpp b/src/mbgl/tile/tile.hpp
index 95ee0bc37e..17a16b4666 100644
--- a/src/mbgl/tile/tile.hpp
+++ b/src/mbgl/tile/tile.hpp
@@ -58,10 +58,16 @@ public:
assert(false);
return nullptr;
}
- // Updates the contained layer render data with the given properties.
+ // Notifies this tile of the updated layer properties.
+ //
+ // Tile implementation should update the contained layer
+ // render data with the given properties.
+ //
// Returns `true` if the corresponding render layer data is present in this tile (and i.e. it
// was succesfully updated); returns `false` otherwise.
- virtual bool updateLayerProperties(const Immutable<style::LayerProperties>&) { return true; }
+ virtual bool layerPropertiesUpdated(const Immutable<style::LayerProperties>& layerProperties) {
+ return bool(getBucket(*layerProperties->baseImpl));
+ }
virtual void setShowCollisionBoxes(const bool) {}
virtual void setLayers(const std::vector<Immutable<style::LayerProperties>>&) {}
virtual void setMask(TileMask&&) {}
diff --git a/src/mbgl/util/i18n.cpp b/src/mbgl/util/i18n.cpp
index d98ac8dbf9..d3364f5e53 100644
--- a/src/mbgl/util/i18n.cpp
+++ b/src/mbgl/util/i18n.cpp
@@ -415,7 +415,8 @@ bool allowsIdeographicBreaking(char16_t chr) {
bool allowsFixedWidthGlyphGeneration(char16_t chr) {
// Mirrors conservative set of characters used in glyph_manager.js/_tinySDF
- return isInCJKUnifiedIdeographs(chr) || isInHangulSyllables(chr);
+ return isInCJKUnifiedIdeographs(chr) || isInHangulSyllables(chr)
+ || isInKatakana(chr) || isInHiragana(chr);
}
bool allowsVerticalWritingMode(const std::u16string& string) {
diff --git a/test/fixtures/expression_equality/number-format-currency.a.json b/test/fixtures/expression_equality/number-format-currency.a.json
new file mode 100644
index 0000000000..d22fb5bc4a
--- /dev/null
+++ b/test/fixtures/expression_equality/number-format-currency.a.json
@@ -0,0 +1,9 @@
+[
+ "number-format",
+ 123456.789,
+ {
+ "locale": "de-DE",
+ "currency": "EUR"
+ }
+]
+
diff --git a/test/fixtures/expression_equality/number-format-currency.b.json b/test/fixtures/expression_equality/number-format-currency.b.json
new file mode 100644
index 0000000000..c85b457142
--- /dev/null
+++ b/test/fixtures/expression_equality/number-format-currency.b.json
@@ -0,0 +1,9 @@
+[
+ "number-format",
+ 123456.789,
+ {
+ "locale": "ja-JP",
+ "currency": "JPY"
+ }
+]
+
diff --git a/test/fixtures/expression_equality/number-format-default.a.json b/test/fixtures/expression_equality/number-format-default.a.json
new file mode 100644
index 0000000000..f41348d8f8
--- /dev/null
+++ b/test/fixtures/expression_equality/number-format-default.a.json
@@ -0,0 +1,5 @@
+[
+ "number-format",
+ 123456.789, {}
+]
+
diff --git a/test/fixtures/expression_equality/number-format-default.b.json b/test/fixtures/expression_equality/number-format-default.b.json
new file mode 100644
index 0000000000..8bbc1bcfb5
--- /dev/null
+++ b/test/fixtures/expression_equality/number-format-default.b.json
@@ -0,0 +1,5 @@
+[
+ "number-format",
+ -123456.789, {}
+]
+
diff --git a/test/fixtures/expression_equality/number-format-precision.a.json b/test/fixtures/expression_equality/number-format-precision.a.json
new file mode 100644
index 0000000000..24d56d8d10
--- /dev/null
+++ b/test/fixtures/expression_equality/number-format-precision.a.json
@@ -0,0 +1,10 @@
+[
+ "number-format",
+ 987654321.23456789,
+ {
+ "locale": "en-US",
+ "min-fraction-digits": 15,
+ "max-fraction-digits": 20
+ }
+]
+
diff --git a/test/fixtures/expression_equality/number-format-precision.b.json b/test/fixtures/expression_equality/number-format-precision.b.json
new file mode 100644
index 0000000000..d8935f9f5f
--- /dev/null
+++ b/test/fixtures/expression_equality/number-format-precision.b.json
@@ -0,0 +1,10 @@
+[
+ "number-format",
+ 987654321.23456789,
+ {
+ "locale": "en-US",
+ "min-fraction-digits": 2,
+ "max-fraction-digits": 4
+ }
+]
+
diff --git a/test/fixtures/local_glyphs/mixed.json b/test/fixtures/local_glyphs/mixed.json
index e07d429753..b982fd9641 100644
--- a/test/fixtures/local_glyphs/mixed.json
+++ b/test/fixtures/local_glyphs/mixed.json
@@ -11,7 +11,7 @@
{
"type": "Feature",
"properties": {
- "name": "身什戰 1"
+ "name": "身什戰アあ1"
},
"geometry": {
"type": "LineString",
@@ -30,7 +30,7 @@
{
"type": "Feature",
"properties": {
- "name": "two 身什戰"
+ "name": "A身什戰アあ"
},
"geometry": {
"type": "LineString",
@@ -49,7 +49,7 @@
{
"type": "Feature",
"properties": {
- "name": "身什戰33"
+ "name": "身什戰アあA"
},
"geometry": {
"type": "LineString",
@@ -68,7 +68,7 @@
{
"type": "Feature",
"properties": {
- "name": "身什戰"
+ "name": "身什戰アあ"
},
"geometry": {
"type": "LineString",
@@ -87,7 +87,7 @@
{
"type": "Feature",
"properties": {
- "name": "身什戰 five"
+ "name": "身什戰アあ5"
},
"geometry": {
"type": "LineString",
@@ -106,7 +106,7 @@
{
"type": "Feature",
"properties": {
- "name": "six 身什戰"
+ "name": "B身什戰アあ"
},
"geometry": {
"type": "LineString",
@@ -125,7 +125,7 @@
{
"type": "Feature",
"properties": {
- "name": "身什戰 seven"
+ "name": "身什戰アあS2"
},
"geometry": {
"type": "LineString",
@@ -144,7 +144,7 @@
{
"type": "Feature",
"properties": {
- "name": "eight 身什戰"
+ "name": "8身什戰アあ"
},
"geometry": {
"type": "LineString",
diff --git a/test/fixtures/local_glyphs/no_local/expected.png b/test/fixtures/local_glyphs/no_local/expected.png
index c7b1b828ee..77552c32fa 100644
--- a/test/fixtures/local_glyphs/no_local/expected.png
+++ b/test/fixtures/local_glyphs/no_local/expected.png
Binary files differ
diff --git a/test/fixtures/local_glyphs/no_local_with_content_insets/expected.png b/test/fixtures/local_glyphs/no_local_with_content_insets/expected.png
index f161a95c8e..52ef264590 100644
--- a/test/fixtures/local_glyphs/no_local_with_content_insets/expected.png
+++ b/test/fixtures/local_glyphs/no_local_with_content_insets/expected.png
Binary files differ
diff --git a/test/fixtures/local_glyphs/no_local_with_content_insets_and_pitch/expected.png b/test/fixtures/local_glyphs/no_local_with_content_insets_and_pitch/expected.png
index edb8d07a40..a1aa5fcb52 100644
--- a/test/fixtures/local_glyphs/no_local_with_content_insets_and_pitch/expected.png
+++ b/test/fixtures/local_glyphs/no_local_with_content_insets_and_pitch/expected.png
Binary files differ
diff --git a/test/fixtures/local_glyphs/ping_fang/expected.png b/test/fixtures/local_glyphs/ping_fang/expected.png
index c0ba43bf11..44c24c276a 100644
--- a/test/fixtures/local_glyphs/ping_fang/expected.png
+++ b/test/fixtures/local_glyphs/ping_fang/expected.png
Binary files differ
diff --git a/test/storage/offline_database.test.cpp b/test/storage/offline_database.test.cpp
index fc1efc1a31..58dd7e9e6a 100644
--- a/test/storage/offline_database.test.cpp
+++ b/test/storage/offline_database.test.cpp
@@ -318,7 +318,7 @@ TEST(OfflineDatabase, TEST_REQUIRES_WRITE(GetResourceFromOfflineRegion)) {
deleteDatabaseFiles();
util::copyFile(filename, "test/fixtures/offline_database/satellite_test.db");
- OfflineDatabase db(filename, mapbox::sqlite::ReadOnly);
+ OfflineDatabase db(filename);
Resource resource = Resource::style("mapbox://styles/mapbox/satellite-v9");
ASSERT_TRUE(db.get(resource));
@@ -515,7 +515,141 @@ TEST(OfflineDatabase, GetRegionDefinition) {
);
}
-TEST(OfflineDatabase, DeleteRegion) {
+TEST(OfflineDatabase, TEST_REQUIRES_WRITE(MaximumAmbientCacheSize)) {
+ FixtureLog log;
+ deleteDatabaseFiles();
+
+ auto databaseSize = [] {
+ return util::read_file(filename).size();
+ };
+
+ {
+ OfflineDatabase db(filename);
+ }
+
+ size_t initialSize = util::read_file(filename).size();
+ size_t maximumSize = 50 * 1024 * 1024;
+
+ Response response;
+ response.data = randomString(100 * 1024);
+
+ {
+ OfflineDatabase db(filename);
+ db.setMaximumAmbientCacheSize(maximumSize); // 50 MB
+
+ OfflineTilePyramidRegionDefinition definition{ "mapbox://style", LatLngBounds::hull({1, 2}, {3, 4}), 5, 6, 2.0, true };
+ OfflineRegionMetadata metadata{{ 1, 2, 3 }};
+
+ auto region = db.createRegion(definition, metadata);
+
+ // Add 100 MB of resources (50/50 ambient/region)
+ for (unsigned i = 0; i < 250; ++i) {
+ const Resource ambientTile = Resource::tile("mapbox://ambient_tile_" + std::to_string(i), 1, 0, 0, 0, Tileset::Scheme::XYZ);
+ db.put(ambientTile, response);
+
+ const Resource ambientStyle = Resource::style("mapbox://ambient_style_" + std::to_string(i));
+ db.put(ambientStyle, response);
+
+ const Resource regionTile = Resource::tile("mapbox://region_tile_" + std::to_string(i), 1, 0, 0, 0, Tileset::Scheme::XYZ);
+ db.putRegionResource(region->getID(), regionTile, response);
+
+ const Resource regionStyle = Resource::style("mapbox://region_style_" + std::to_string(i));
+ db.putRegionResource(region->getID(), regionStyle, response);
+ }
+ }
+
+ // We are adding about 50 MB of "region" data and 50 MB,
+ // of "ambient" data. The effective size of the ambient
+ // cache will be zero here because it will try to make
+ // room for the region data.
+ EXPECT_GE(databaseSize(), maximumSize);
+ EXPECT_LE(databaseSize(), 60 * 1024 * 1024);
+
+ maximumSize = 30 * 1024 * 1024;
+
+ {
+ OfflineDatabase db(filename);
+ db.setMaximumAmbientCacheSize(maximumSize); // 30 MB
+ }
+
+ // Setting a new size to the ambient cache should have no
+ // effect because it is all taken by offline region anyway.
+ EXPECT_GE(databaseSize(), maximumSize);
+ EXPECT_LE(databaseSize(), 60 * 1024 * 1024);
+
+ {
+ OfflineDatabase db(filename);
+ db.setMaximumAmbientCacheSize(maximumSize); // 30 MB
+ db.deleteRegion(std::move(db.listRegions().value()[0]));
+ }
+
+ // After deleting the offline region, the data will migrate
+ // to the ambient cache, respecting the size defined.
+ EXPECT_LE(databaseSize(), maximumSize);
+ EXPECT_GE(databaseSize(), maximumSize / 2);
+
+ {
+ OfflineDatabase db(filename);
+ db.setMaximumAmbientCacheSize(maximumSize * 2); // 60 MB
+ }
+
+ // Doubling the size should have no effect if
+ // we don't and new tiles and if the ambient cache
+ // is already under the maximum size.
+ EXPECT_LE(databaseSize(), maximumSize);
+ EXPECT_GE(databaseSize(), maximumSize / 2);
+
+ {
+ OfflineDatabase db(filename);
+ db.setMaximumAmbientCacheSize(maximumSize); // 30 MB
+
+ // Add ~50 MB in ambient cache data.
+ for (unsigned i = 0; i < 250; ++i) {
+ const Resource ambientTile = Resource::tile("mapbox://ambient_tile_" + std::to_string(i), 1, 0, 0, 0, Tileset::Scheme::XYZ);
+ db.put(ambientTile, response);
+
+ const Resource ambientStyle = Resource::style("mapbox://ambient_style_" + std::to_string(i));
+ db.put(ambientStyle, response);
+ }
+ }
+
+ // Only ambient cache now, so it should respect
+ // the established size.
+ EXPECT_LE(databaseSize(), maximumSize);
+ EXPECT_GE(databaseSize(), maximumSize / 2);
+
+ maximumSize = 20 * 1024 * 1024;
+
+ {
+ OfflineDatabase db(filename);
+ db.setMaximumAmbientCacheSize(maximumSize); // 20 MB
+ }
+
+ // Should shrink again.
+ EXPECT_LE(databaseSize(), maximumSize);
+ EXPECT_GE(databaseSize(), initialSize);
+
+ {
+ OfflineDatabase db(filename);
+ db.setMaximumAmbientCacheSize(0);
+
+ for (unsigned i = 0; i < 5; ++i) {
+ const Resource ambientTile = Resource::tile("mapbox://ambient_tile_" + std::to_string(i), 1, 0, 0, 0, Tileset::Scheme::XYZ);
+ db.put(ambientTile, response);
+ ASSERT_FALSE(db.get(ambientTile));
+
+ const Resource ambientStyle = Resource::style("mapbox://ambient_style_" + std::to_string(i));
+ db.put(ambientStyle, response);
+ ASSERT_FALSE(db.get(ambientStyle));
+ }
+ }
+
+ // Setting the size to zero should effectively
+ // clear the cache now.
+ EXPECT_EQ(initialSize, util::read_file(filename).size());
+}
+
+TEST(OfflineDatabase, TEST_REQUIRES_WRITE(DeleteRegion)) {
FixtureLog log;
deleteDatabaseFiles();
@@ -536,9 +670,12 @@ TEST(OfflineDatabase, DeleteRegion) {
auto region = db.createRegion(definition, metadata);
- for (unsigned i = 0; i < 100; ++i) {
+ for (unsigned i = 0; i < 50; ++i) {
const Resource tile = Resource::tile("mapbox://tile_" + std::to_string(i), 1, 0, 0, 0, Tileset::Scheme::XYZ);
db.putRegionResource(region->getID(), tile, response);
+
+ const Resource style = Resource::style("mapbox://style_" + std::to_string(i));
+ db.putRegionResource(region->getID(), style, response);
}
db.deleteRegion(std::move(*region));
@@ -553,13 +690,111 @@ TEST(OfflineDatabase, DeleteRegion) {
// After clearing the cache, the size of the database
// should get back to the original size.
- db.clearTileCache();
+ db.clearAmbientCache();
}
EXPECT_EQ(initialSize, util::read_file(filename).size());
EXPECT_EQ(0u, log.uncheckedCount());
}
+TEST(OfflineDatabase, MapboxTileLimitExceeded) {
+ FixtureLog log;
+
+ uint64_t limit = 60;
+
+ OfflineDatabase db(":memory:");
+ db.setOfflineMapboxTileCountLimit(limit);
+
+ Response response;
+ response.data = randomString(4096);
+
+ auto insertAmbientTile = [&](unsigned i) {
+ const Resource ambientTile = Resource::tile("mapbox://ambient_tile_" + std::to_string(i), 1, 0, 0, 0, Tileset::Scheme::XYZ);
+ db.put(ambientTile, response);
+ };
+
+ auto insertRegionTile = [&](int64_t regionID, unsigned i) {
+ const Resource tile = Resource::tile("mapbox://region_tile_" + std::to_string(i), 1, 0, 0, 0, Tileset::Scheme::XYZ);
+ db.putRegionResource(regionID, tile, response);
+ };
+
+ OfflineTilePyramidRegionDefinition definition1{ "mapbox://style1", LatLngBounds::hull({1, 2}, {3, 4}), 5, 6, 2.0, true };
+ OfflineRegionMetadata metadata1{{ 1, 2, 3 }};
+
+ OfflineTilePyramidRegionDefinition definition2{ "mapbox://style2", LatLngBounds::hull({1, 2}, {3, 4}), 5, 6, 2.0, true };
+ OfflineRegionMetadata metadata2{{ 1, 2, 3 }};
+
+ auto region1 = db.createRegion(definition1, metadata1);
+ auto region2 = db.createRegion(definition2, metadata2);
+
+ // Fine because tile limit only affects offline region.
+ for (unsigned i = 0; i < limit * 2; ++i) {
+ insertAmbientTile(i);
+ }
+
+ ASSERT_EQ(db.getOfflineMapboxTileCount(), 0);
+
+ // Fine because this region is under the tile limit.
+ for (unsigned i = 0; i < limit - 10; ++i) {
+ insertRegionTile(region1->getID(), i);
+ }
+
+ ASSERT_EQ(db.getOfflineMapboxTileCount(), limit - 10);
+
+ // Fine because this region + the previous is at the limit.
+ for (unsigned i = limit; i < limit + 10; ++i) {
+ insertRegionTile(region2->getID(), i);
+ }
+
+ ASSERT_EQ(db.getOfflineMapboxTileCount(), limit);
+
+ // Full.
+ ASSERT_THROW(insertRegionTile(region1->getID(), 200), MapboxTileLimitExceededException);
+ ASSERT_THROW(insertRegionTile(region2->getID(), 201), MapboxTileLimitExceededException);
+
+ // These tiles are already on respective
+ // regions.
+ insertRegionTile(region1->getID(), 0);
+ insertRegionTile(region2->getID(), 60);
+
+ // Should be fine, ambient tile.
+ insertAmbientTile(333);
+
+ // Also fine, not Mapbox.
+ const Resource notMapboxTile = Resource::tile("foobar://region_tile", 1, 0, 0, 0, Tileset::Scheme::XYZ);
+ db.putRegionResource(region1->getID(), notMapboxTile, response);
+
+ // These tiles are not on the region they are
+ // being added to, but exist on another region,
+ // so they do not add to the total size.
+ insertRegionTile(region2->getID(), 0);
+ insertRegionTile(region1->getID(), 60);
+
+ ASSERT_EQ(db.getOfflineMapboxTileCount(), limit);
+
+ // The tile 1 belongs to two regions and will
+ // still count as resource.
+ db.deleteRegion(std::move(*region2));
+
+ ASSERT_EQ(db.getOfflineMapboxTileCount(), 51);
+
+ // Add new tiles to the region 1. We are adding
+ // 10, which would blow up the limit if it wasn't
+ // for the fact that tile 60 is already on the
+ // database and will not count.
+ for (unsigned i = limit; i < limit + 10; ++i) {
+ insertRegionTile(region1->getID(), i);
+ }
+
+ // Full again.
+ ASSERT_THROW(insertRegionTile(region1->getID(), 202), MapboxTileLimitExceededException);
+
+ db.deleteRegion(std::move(*region1));
+
+ ASSERT_EQ(0u, db.listRegions().value().size());
+ ASSERT_EQ(0u, log.uncheckedCount());
+}
+
TEST(OfflineDatabase, Invalidate) {
using namespace std::chrono_literals;
@@ -571,49 +806,73 @@ TEST(OfflineDatabase, Invalidate) {
response.mustRevalidate = false;
response.expires = util::now() + 1h;
- const Resource ambient = Resource::tile("mapbox://tile_ambient", 1, 0, 0, 0, Tileset::Scheme::XYZ);
- db.put(ambient, response);
+ const Resource ambientTile = Resource::tile("mapbox://tile_ambient", 1, 0, 0, 0, Tileset::Scheme::XYZ);
+ db.put(ambientTile, response);
+
+ const Resource ambientStyle = Resource::style("mapbox://style_ambient");
+ db.put(ambientStyle, response);
OfflineTilePyramidRegionDefinition definition { "mapbox://style", LatLngBounds::hull({1, 2}, {3, 4}), 5, 6, 2.0, true };
OfflineRegionMetadata metadata {{ 1, 2, 3 }};
auto region1 = db.createRegion(definition, metadata);
- const Resource offline1 = Resource::tile("mapbox://tile_offline_region1", 1.0, 0, 0, 0, Tileset::Scheme::XYZ);
- db.putRegionResource(region1->getID(), offline1, response);
+ const Resource region1Tile = Resource::tile("mapbox://tile_offline_region1", 1.0, 0, 0, 0, Tileset::Scheme::XYZ);
+ db.putRegionResource(region1->getID(), region1Tile, response);
+
+ const Resource region1Style = Resource::style("mapbox://style_offline_region1");
+ db.putRegionResource(region1->getID(), region1Style, response);
auto region2 = db.createRegion(definition, metadata);
- const Resource offline2 = Resource::tile("mapbox://tile_offline_region2", 1.0, 0, 0, 0, Tileset::Scheme::XYZ);
- db.putRegionResource(region2->getID(), offline2, response);
+ const Resource region2Tile = Resource::tile("mapbox://tile_offline_region2", 1.0, 0, 0, 0, Tileset::Scheme::XYZ);
+ db.putRegionResource(region2->getID(), region2Tile, response);
+
+ const Resource region2Style = Resource::style("mapbox://style_offline_region2");
+ db.putRegionResource(region2->getID(), region2Style, response);
// Prior to invalidation, all tiles are usable.
- EXPECT_TRUE(db.get(ambient)->isUsable());
- EXPECT_TRUE(db.get(offline1)->isUsable());
- EXPECT_TRUE(db.get(offline2)->isUsable());
+ EXPECT_TRUE(db.get(ambientTile)->isUsable());
+ EXPECT_TRUE(db.get(ambientStyle)->isUsable());
+ EXPECT_TRUE(db.get(region1Tile)->isUsable());
+ EXPECT_TRUE(db.get(region1Style)->isUsable());
+ EXPECT_TRUE(db.get(region2Tile)->isUsable());
+ EXPECT_TRUE(db.get(region2Style)->isUsable());
// Invalidate a region will not invalidate ambient
// tiles or other regions.
EXPECT_TRUE(db.invalidateRegion(region1->getID()) == nullptr);
- EXPECT_TRUE(db.get(ambient)->isUsable());
- EXPECT_FALSE(db.get(offline1)->isUsable());
- EXPECT_TRUE(db.get(offline2)->isUsable());
+ EXPECT_TRUE(db.get(ambientTile)->isUsable());
+ EXPECT_TRUE(db.get(ambientStyle)->isUsable());
+ EXPECT_FALSE(db.get(region1Tile)->isUsable());
+ EXPECT_FALSE(db.get(region1Style)->isUsable());
+ EXPECT_TRUE(db.get(region2Tile)->isUsable());
+ EXPECT_TRUE(db.get(region2Style)->isUsable());
// Invalidate the ambient cache will not invalidate
// the regions that are still valid.
- EXPECT_TRUE(db.invalidateTileCache() == nullptr);
+ EXPECT_TRUE(db.invalidateAmbientCache() == nullptr);
- EXPECT_FALSE(db.get(ambient)->isUsable());
- EXPECT_FALSE(db.get(offline1)->isUsable());
- EXPECT_TRUE(db.get(offline2)->isUsable());
+ EXPECT_FALSE(db.get(ambientTile)->isUsable());
+ EXPECT_FALSE(db.get(ambientStyle)->isUsable());
+ EXPECT_FALSE(db.get(region1Tile)->isUsable());
+ EXPECT_FALSE(db.get(region1Style)->isUsable());
+ EXPECT_TRUE(db.get(region2Tile)->isUsable());
+ EXPECT_TRUE(db.get(region2Style)->isUsable());
// Sanity check.
- EXPECT_TRUE(db.get(ambient)->expires < util::now());
- EXPECT_TRUE(db.get(offline1)->expires < util::now());
- EXPECT_TRUE(db.get(offline2)->expires > util::now());
-
- EXPECT_TRUE(db.get(ambient)->mustRevalidate);
- EXPECT_TRUE(db.get(offline1)->mustRevalidate);
- EXPECT_FALSE(db.get(offline2)->mustRevalidate);
+ EXPECT_TRUE(db.get(ambientTile)->expires < util::now());
+ EXPECT_TRUE(db.get(ambientStyle)->expires < util::now());
+ EXPECT_TRUE(db.get(region1Tile)->expires < util::now());
+ EXPECT_TRUE(db.get(region1Style)->expires < util::now());
+ EXPECT_TRUE(db.get(region2Tile)->expires > util::now());
+ EXPECT_TRUE(db.get(region2Style)->expires > util::now());
+
+ EXPECT_TRUE(db.get(ambientTile)->mustRevalidate);
+ EXPECT_TRUE(db.get(ambientStyle)->mustRevalidate);
+ EXPECT_TRUE(db.get(region1Tile)->mustRevalidate);
+ EXPECT_TRUE(db.get(region1Style)->mustRevalidate);
+ EXPECT_FALSE(db.get(region2Tile)->mustRevalidate);
+ EXPECT_FALSE(db.get(region2Style)->mustRevalidate);
// Should not throw.
EXPECT_TRUE(db.invalidateRegion(region2->getID()) == nullptr);
@@ -627,7 +886,7 @@ TEST(OfflineDatabase, Invalidate) {
EXPECT_EQ(0u, log.uncheckedCount());
}
-TEST(OfflineDatabase, ClearTileCache) {
+TEST(OfflineDatabase, TEST_REQUIRES_WRITE(ClearAmbientCache)) {
FixtureLog log;
deleteDatabaseFiles();
@@ -643,12 +902,15 @@ TEST(OfflineDatabase, ClearTileCache) {
OfflineDatabase db(filename);
- for (unsigned i = 0; i < 100; ++i) {
+ for (unsigned i = 0; i < 50; ++i) {
const Resource tile = Resource::tile("mapbox://tile_" + std::to_string(i), 1, 0, 0, 0, Tileset::Scheme::XYZ);
db.put(tile, response);
+
+ const Resource style = Resource::style("mapbox://style_" + std::to_string(i));
+ db.put(style, response);
}
- db.clearTileCache();
+ db.clearAmbientCache();
}
EXPECT_EQ(initialSize, util::read_file(filename).size());
@@ -693,7 +955,7 @@ TEST(OfflineDatabase, TEST_REQUIRES_WRITE(ConcurrentUse)) {
EXPECT_TRUE(bool(db2.get(fixture::resource)));
if (i == 50) {
- db2.clearTileCache();
+ db2.clearAmbientCache();
}
}
});
@@ -725,7 +987,8 @@ TEST(OfflineDatabase, PutReturnsSize) {
TEST(OfflineDatabase, PutEvictsLeastRecentlyUsedResources) {
FixtureLog log;
- OfflineDatabase db(":memory:", 1024 * 100);
+ OfflineDatabase db(":memory:");
+ db.setMaximumAmbientCacheSize(1024 * 100);
Response response;
response.data = randomString(1024);
@@ -743,7 +1006,9 @@ TEST(OfflineDatabase, PutEvictsLeastRecentlyUsedResources) {
TEST(OfflineDatabase, PutRegionResourceDoesNotEvict) {
FixtureLog log;
- OfflineDatabase db(":memory:", 1024 * 100);
+ OfflineDatabase db(":memory:");
+ db.setMaximumAmbientCacheSize(1024 * 100);
+
OfflineTilePyramidRegionDefinition definition { "", LatLngBounds::world(), 0, INFINITY, 1.0, true };
auto region = db.createRegion(definition, OfflineRegionMetadata());
ASSERT_TRUE(region);
@@ -763,7 +1028,8 @@ TEST(OfflineDatabase, PutRegionResourceDoesNotEvict) {
TEST(OfflineDatabase, PutFailsWhenEvictionInsuffices) {
FixtureLog log;
- OfflineDatabase db(":memory:", 1024 * 100);
+ OfflineDatabase db(":memory:");
+ db.setMaximumAmbientCacheSize(1024 * 100);
Response big;
big.data = randomString(1024 * 100);
@@ -819,7 +1085,9 @@ TEST(OfflineDatabase, GetRegionCompletedStatus) {
TEST(OfflineDatabase, HasRegionResource) {
FixtureLog log;
- OfflineDatabase db(":memory:", 1024 * 100);
+ OfflineDatabase db(":memory:");
+ db.setMaximumAmbientCacheSize(1024 * 100);
+
OfflineTilePyramidRegionDefinition definition { "", LatLngBounds::world(), 0, INFINITY, 1.0, true };
auto region = db.createRegion(definition, OfflineRegionMetadata());
ASSERT_TRUE(region);
@@ -843,7 +1111,9 @@ TEST(OfflineDatabase, HasRegionResource) {
TEST(OfflineDatabase, HasRegionResourceTile) {
FixtureLog log;
- OfflineDatabase db(":memory:", 1024 * 100);
+ OfflineDatabase db(":memory:");
+ db.setMaximumAmbientCacheSize(1024 * 100);
+
OfflineTilePyramidRegionDefinition definition { "", LatLngBounds::world(), 0, INFINITY, 1.0, false };
auto region = db.createRegion(definition, OfflineRegionMetadata());
ASSERT_TRUE(region);
@@ -938,7 +1208,9 @@ TEST(OfflineDatabase, OfflineMapboxTileCount) {
TEST(OfflineDatabase, BatchInsertion) {
FixtureLog log;
- OfflineDatabase db(":memory:", 1024 * 100);
+ OfflineDatabase db(":memory:");
+ db.setMaximumAmbientCacheSize(1024 * 100);
+
OfflineTilePyramidRegionDefinition definition { "", LatLngBounds::world(), 0, INFINITY, 1.0, true };
auto region = db.createRegion(definition, OfflineRegionMetadata());
ASSERT_TRUE(region);
@@ -963,8 +1235,10 @@ TEST(OfflineDatabase, BatchInsertion) {
TEST(OfflineDatabase, BatchInsertionMapboxTileCountExceeded) {
FixtureLog log;
- OfflineDatabase db(":memory:", 1024 * 100);
+ OfflineDatabase db(":memory:");
db.setOfflineMapboxTileCountLimit(1);
+ db.setMaximumAmbientCacheSize(1024 * 100);
+
OfflineTilePyramidRegionDefinition definition { "", LatLngBounds::world(), 0, INFINITY, 1.0, false };
auto region = db.createRegion(definition, OfflineRegionMetadata());
ASSERT_TRUE(region);
@@ -1001,7 +1275,9 @@ TEST(OfflineDatabase, MigrateFromV2Schema) {
util::copyFile(filename, "test/fixtures/offline_database/v2.db");
{
- OfflineDatabase db(filename, 0);
+ OfflineDatabase db(filename);
+ db.setMaximumAmbientCacheSize(0);
+
auto regions = db.listRegions();
ASSERT_TRUE(regions);
for (auto& region : regions.value()) {
@@ -1023,7 +1299,9 @@ TEST(OfflineDatabase, MigrateFromV3Schema) {
util::copyFile(filename, "test/fixtures/offline_database/v3.db");
{
- OfflineDatabase db(filename, 0);
+ OfflineDatabase db(filename);
+ db.setMaximumAmbientCacheSize(0);
+
auto regions = db.listRegions().value();
for (auto& region : regions) {
db.deleteRegion(std::move(region));
@@ -1042,7 +1320,9 @@ TEST(OfflineDatabase, MigrateFromV4Schema) {
util::copyFile(filename, "test/fixtures/offline_database/v4.db");
{
- OfflineDatabase db(filename, 0);
+ OfflineDatabase db(filename);
+ db.setMaximumAmbientCacheSize(0);
+
auto regions = db.listRegions().value();
for (auto& region : regions) {
db.deleteRegion(std::move(region));
@@ -1068,7 +1348,9 @@ TEST(OfflineDatabase, MigrateFromV5Schema) {
util::copyFile(filename, "test/fixtures/offline_database/v5.db");
{
- OfflineDatabase db(filename, 0);
+ OfflineDatabase db(filename);
+ db.setMaximumAmbientCacheSize(0);
+
auto regions = db.listRegions().value();
for (auto& region : regions) {
db.deleteRegion(std::move(region));
@@ -1096,7 +1378,8 @@ TEST(OfflineDatabase, DowngradeSchema) {
util::copyFile(filename, "test/fixtures/offline_database/v999.db");
{
- OfflineDatabase db(filename, 0);
+ OfflineDatabase db(filename);
+ db.setMaximumAmbientCacheSize(0);
}
EXPECT_EQ(6, databaseUserVersion(filename));
@@ -1474,7 +1757,7 @@ TEST(OfflineDatabase, TEST_REQUIRES_WRITE(MergeDatabaseWithDiskFull)) {
}
#endif // __QT__
-TEST(OfflineDatabse, ChangePath) {
+TEST(OfflineDatabase, ChangePath) {
std::string newPath("test/fixtures/offline_database/test.db");
OfflineDatabase db(":memory:");
db.changePath(newPath);
@@ -1482,13 +1765,13 @@ TEST(OfflineDatabse, ChangePath) {
util::deleteFile(newPath);
}
-TEST(OfflineDatabse, resetCache) {
+TEST(OfflineDatabase, ResetDatabase) {
FixtureLog log;
deleteDatabaseFiles();
util::copyFile(filename, "test/fixtures/offline_database/satellite_test.db");
OfflineDatabase db(filename);
- auto result = db.resetCache();
+ auto result = db.resetDatabase();
EXPECT_FALSE(result);
auto regions = db.listRegions().value();
diff --git a/test/style/expression/expression.test.cpp b/test/style/expression/expression.test.cpp
index 42a8fdd726..8e2acfcd32 100644
--- a/test/style/expression/expression.test.cpp
+++ b/test/style/expression/expression.test.cpp
@@ -36,10 +36,8 @@ TEST(Expression, IsExpression) {
// TODO: "feature-state": https://github.com/mapbox/mapbox-gl-native/issues/12613
// TODO: "interpolate-hcl": https://github.com/mapbox/mapbox-gl-native/issues/8720
// TODO: "interpolate-lab": https://github.com/mapbox/mapbox-gl-native/issues/8720
- // TODO: "number-format": https://github.com/mapbox/mapbox-gl-native/issues/13632
// TODO: "accumulated": https://github.com/mapbox/mapbox-gl-native/issues/14043
- if (name == "feature-state" || name == "interpolate-hcl" || name == "interpolate-lab" || name == "number-format" ||
- name == "accumulated") {
+ if (name == "feature-state" || name == "interpolate-hcl" || name == "interpolate-lab" || name == "accumulated") {
if (expression::isExpression(conversion::Convertible(expression))) {
ASSERT_TRUE(false) << "Expression name" << name << "is implemented - please update Expression.IsExpression test.";
}
diff --git a/vendor/boost b/vendor/boost
-Subproject 2499bc09829ad3bb913271f4b6889df8e4d692b
+Subproject e822019d0f36fe43103cf4208d5cccea526a15b
diff --git a/vendor/boost-files.json b/vendor/boost-files.json
index 3ee6f74bf4..3e64d09959 100644
--- a/vendor/boost-files.json
+++ b/vendor/boost-files.json
@@ -44,6 +44,11 @@
"boost/algorithm/string/trim.hpp": "vendor/boost/include/boost/algorithm/string/trim.hpp",
"boost/algorithm/string/yes_no_type.hpp": "vendor/boost/include/boost/algorithm/string/yes_no_type.hpp",
"boost/aligned_storage.hpp": "vendor/boost/include/boost/aligned_storage.hpp",
+ "boost/archive/iterators/base64_from_binary.hpp": "vendor/boost/include/boost/archive/iterators/base64_from_binary.hpp",
+ "boost/archive/iterators/dataflow_exception.hpp": "vendor/boost/include/boost/archive/iterators/dataflow_exception.hpp",
+ "boost/archive/iterators/insert_linebreaks.hpp": "vendor/boost/include/boost/archive/iterators/insert_linebreaks.hpp",
+ "boost/archive/iterators/ostream_iterator.hpp": "vendor/boost/include/boost/archive/iterators/ostream_iterator.hpp",
+ "boost/archive/iterators/transform_width.hpp": "vendor/boost/include/boost/archive/iterators/transform_width.hpp",
"boost/array.hpp": "vendor/boost/include/boost/array.hpp",
"boost/assert.hpp": "vendor/boost/include/boost/assert.hpp",
"boost/bind/mem_fn.hpp": "vendor/boost/include/boost/bind/mem_fn.hpp",
diff --git a/vendor/filesystem b/vendor/filesystem
new file mode 160000
+Subproject 091c08663ac3e38aea1ccaeae235340f5154f5a
diff --git a/vendor/filesystem-files.json b/vendor/filesystem-files.json
new file mode 100644
index 0000000000..d65e9c828e
--- /dev/null
+++ b/vendor/filesystem-files.json
@@ -0,0 +1,13 @@
+{
+ "//": "This file is generated. Do not edit. Regenerate it with scripts/generate-file-lists.js",
+ "sources": [],
+ "public_headers": {
+ "ghc/filesystem.hpp": "vendor/filesystem/include/ghc/filesystem.hpp",
+ "ghc/fs_fwd.hpp": "vendor/filesystem/include/ghc/fs_fwd.hpp",
+ "ghc/fs_impl.hpp": "vendor/filesystem/include/ghc/fs_impl.hpp",
+ "ghc/fs_std.hpp": "vendor/filesystem/include/ghc/fs_std.hpp",
+ "ghc/fs_std_fwd.hpp": "vendor/filesystem/include/ghc/fs_std_fwd.hpp",
+ "ghc/fs_std_impl.hpp": "vendor/filesystem/include/ghc/fs_std_impl.hpp"
+ },
+ "private_headers": {}
+}
diff --git a/vendor/filesystem.cmake b/vendor/filesystem.cmake
new file mode 100644
index 0000000000..fe2701890d
--- /dev/null
+++ b/vendor/filesystem.cmake
@@ -0,0 +1,5 @@
+add_library(filesystem INTERFACE)
+
+target_include_directories(filesystem SYSTEM INTERFACE
+ ${CMAKE_SOURCE_DIR}/vendor/filesystem/include
+)
diff --git a/vendor/mapbox-gl-styles b/vendor/mapbox-gl-styles
new file mode 160000
+Subproject 322855f2e88ff82695d1c7bc0004b135417768f
diff --git a/vendor/mvt-fixtures b/vendor/mvt-fixtures
new file mode 160000
+Subproject 8423de2d90ab020781b12698278dc116f3b0a82