From b11134a301f720097183adc26c99fd6f7a85cf4f Mon Sep 17 00:00:00 2001 From: alasram Date: Wed, 4 Jun 2025 05:49:41 -0700 Subject: [PATCH] Add Tile LOD controls (#2958) Co-authored-by: Bart Louwers Co-authored-by: Adrian Cojocaru Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- benchmark/util/tilecover.benchmark.cpp | 2 +- include/mbgl/map/map.hpp | 32 ++++ .../src/cpp/native_map_view.cpp | 40 +++++ .../src/cpp/native_map_view.hpp | 16 ++ .../maplibre/android/maps/MapLibreMap.java | 95 +++++++++++ .../org/maplibre/android/maps/NativeMap.java | 16 ++ .../maplibre/android/maps/NativeMapView.java | 88 ++++++++++ platform/glfw/glfw_view.cpp | 86 +++++++++- platform/ios/app/MBXViewController.mm | 125 +++++++++++++- platform/ios/src/MLNMapView.h | 44 +++++ platform/ios/src/MLNMapView.mm | 40 +++++ platform/macos/src/MLNMapView.h | 44 +++++ platform/macos/src/MLNMapView.mm | 40 +++++ src/mbgl/map/map.cpp | 32 ++++ src/mbgl/map/map_impl.cpp | 6 +- src/mbgl/map/map_impl.hpp | 7 + .../layers/render_background_layer.cpp | 9 +- src/mbgl/renderer/paint_parameters.cpp | 10 +- src/mbgl/renderer/paint_parameters.hpp | 9 +- src/mbgl/renderer/render_orchestrator.cpp | 4 + src/mbgl/renderer/renderer_impl.cpp | 11 +- .../renderer/sources/render_image_source.cpp | 4 +- src/mbgl/renderer/tile_parameters.hpp | 5 + src/mbgl/renderer/tile_pyramid.cpp | 16 +- src/mbgl/renderer/update_parameters.hpp | 6 + src/mbgl/util/tile_cover.cpp | 31 ++-- src/mbgl/util/tile_cover.hpp | 13 +- test/CMakeLists.txt | 1 + test/fixtures/tile_lod/disabled/expected.png | Bin 0 -> 872 bytes .../tile_lod/pitchThreshold-line/expected.png | Bin 0 -> 5195 bytes .../pitchThreshold-polyline/expected.png | Bin 0 -> 11076 bytes test/fixtures/tile_lod/radius/expected.png | Bin 0 -> 3800 bytes test/fixtures/tile_lod/scale/expected.png | Bin 0 -> 3416 bytes test/include/mbgl/test/util.hpp | 7 + test/include/mbgl/test/vector_tile_test.hpp | 22 +-- test/src/mbgl/test/util.cpp | 31 +++- test/style/source.test.cpp | 22 +-- test/tile/custom_geometry_tile.test.cpp | 22 +-- test/tile/geojson_tile.test.cpp | 22 +-- test/tile/raster_dem_tile.test.cpp | 22 +-- test/tile/raster_tile.test.cpp | 22 +-- test/tile/tile_lod.test.cpp | 156 ++++++++++++++++++ test/util/tile_cover.test.cpp | 22 +-- 43 files changed, 1066 insertions(+), 114 deletions(-) create mode 100644 test/fixtures/tile_lod/disabled/expected.png create mode 100644 test/fixtures/tile_lod/pitchThreshold-line/expected.png create mode 100644 test/fixtures/tile_lod/pitchThreshold-polyline/expected.png create mode 100644 test/fixtures/tile_lod/radius/expected.png create mode 100644 test/fixtures/tile_lod/scale/expected.png create mode 100644 test/tile/tile_lod.test.cpp diff --git a/benchmark/util/tilecover.benchmark.cpp b/benchmark/util/tilecover.benchmark.cpp index dde9f3e9e..c541323ed 100644 --- a/benchmark/util/tilecover.benchmark.cpp +++ b/benchmark/util/tilecover.benchmark.cpp @@ -26,7 +26,7 @@ static void TileCoverPitchedViewport(benchmark::State& state) { std::size_t length = 0; while (state.KeepRunning()) { - auto tiles = util::tileCover(transform.getState(), 8); + auto tiles = util::tileCover({transform.getState()}, 8); length += tiles.size(); } benchmark::DoNotOptimize(length); diff --git a/include/mbgl/map/map.hpp b/include/mbgl/map/map.hpp index 22972e27d..06b4f29fd 100644 --- a/include/mbgl/map/map.hpp +++ b/include/mbgl/map/map.hpp @@ -161,6 +161,38 @@ public: void setFreeCameraOptions(const FreeCameraOptions& camera); FreeCameraOptions getFreeCameraOptions() const; + // Tile LOD controls + // + /// The number of map tile requests can be reduced by using a lower level + /// of details (Lower zoom level) away from the camera. + /// This can improve performance, particularly when the camera pitch is high. + /// The LOD calculation uses a heuristic based on the distance to the camera + /// view point. The heuristic behavior is controlled with 3 parameters: + /// - `TileLodMinRadius` is a radius around the view point in unit of tiles + /// in which the fine grained zoom level tiles are always used + /// - `TileLodScale` is a scale factor for the distance to the camera view + /// point. A value larger than 1 increases the distance to the camera view + /// point in which case the LOD is reduced + /// - `TileLodPitchThreshold` is the pitch angle in radians above which LOD + /// calculation is performed. + /// LOD calculation is always performed if `TileLodPitchThreshold` is zero. + /// LOD calculation is never performed if `TileLodPitchThreshold` is pi. + /// - `TileLodZoomShift` shifts the the Zoom level used for LOD calculation + /// A value of zero (default) does not change the Zoom level + /// A positive value increases the Zoom level and a negative value decreases + /// the Zoom level + /// A negative values typically improves performance but reduces quality. + /// For instance, a value of -1 reduces the zoom level by 1 and this + /// reduces the number of tiles by a factor of 4 for the same camera view. + void setTileLodMinRadius(double radius); + double getTileLodMinRadius() const; + void setTileLodScale(double scale); + double getTileLodScale() const; + void setTileLodPitchThreshold(double threshold); + double getTileLodPitchThreshold() const; + void setTileLodZoomShift(double shift); + double getTileLodZoomShift() const; + ClientOptions getClientOptions() const; const std::unique_ptr& getActionJournal(); diff --git a/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp b/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp index 9bfb3037d..7fccd8814 100644 --- a/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp @@ -1269,6 +1269,38 @@ jni::jboolean NativeMapView::getTileCacheEnabled(JNIEnv&) { return jni::jboolean(rendererFrontend->getTileCacheEnabled()); } +void NativeMapView::setTileLodMinRadius(JNIEnv&, jni::jdouble radius) { + map->setTileLodMinRadius(radius); +} + +jni::jdouble NativeMapView::getTileLodMinRadius(JNIEnv&) { + return jni::jdouble(map->getTileLodMinRadius()); +} + +void NativeMapView::setTileLodScale(JNIEnv&, jni::jdouble scale) { + map->setTileLodScale(scale); +} + +jni::jdouble NativeMapView::getTileLodScale(JNIEnv&) { + return jni::jdouble(map->getTileLodScale()); +} + +void NativeMapView::setTileLodPitchThreshold(JNIEnv&, jni::jdouble threshold) { + map->setTileLodPitchThreshold(threshold); +} + +jni::jdouble NativeMapView::getTileLodPitchThreshold(JNIEnv&) { + return jni::jdouble(map->getTileLodPitchThreshold()); +} + +void NativeMapView::setTileLodZoomShift(JNIEnv&, jni::jdouble shift) { + map->setTileLodZoomShift(shift); +} + +jni::jdouble NativeMapView::getTileLodZoomShift(JNIEnv&) { + return jni::jdouble(map->getTileLodZoomShift()); +} + mbgl::Map& NativeMapView::getMap() { return *map; } @@ -1400,6 +1432,14 @@ void NativeMapView::registerNative(jni::JNIEnv& env) { METHOD(&NativeMapView::getPrefetchZoomDelta, "nativeGetPrefetchZoomDelta"), METHOD(&NativeMapView::setTileCacheEnabled, "nativeSetTileCacheEnabled"), METHOD(&NativeMapView::getTileCacheEnabled, "nativeGetTileCacheEnabled"), + METHOD(&NativeMapView::setTileLodMinRadius, "nativeSetTileLodMinRadius"), + METHOD(&NativeMapView::getTileLodMinRadius, "nativeGetTileLodMinRadius"), + METHOD(&NativeMapView::setTileLodScale, "nativeSetTileLodScale"), + METHOD(&NativeMapView::getTileLodScale, "nativeGetTileLodScale"), + METHOD(&NativeMapView::setTileLodPitchThreshold, "nativeSetTileLodPitchThreshold"), + METHOD(&NativeMapView::getTileLodPitchThreshold, "nativeGetTileLodPitchThreshold"), + METHOD(&NativeMapView::setTileLodZoomShift, "nativeSetTileLodZoomShift"), + METHOD(&NativeMapView::getTileLodZoomShift, "nativeGetTileLodZoomShift"), METHOD(&NativeMapView::triggerRepaint, "nativeTriggerRepaint"), METHOD(&NativeMapView::isRenderingStatsViewEnabled, "nativeIsRenderingStatsViewEnabled"), METHOD(&NativeMapView::enableRenderingStatsView, "nativeEnableRenderingStatsView")); diff --git a/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp b/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp index bdfb9143c..045b20c57 100644 --- a/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp +++ b/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp @@ -302,6 +302,22 @@ public: jni::jboolean getTileCacheEnabled(JNIEnv&); + void setTileLodMinRadius(JNIEnv&, jni::jdouble); + + jni::jdouble getTileLodMinRadius(JNIEnv&); + + void setTileLodScale(JNIEnv&, jni::jdouble); + + jni::jdouble getTileLodScale(JNIEnv&); + + void setTileLodPitchThreshold(JNIEnv&, jni::jdouble); + + jni::jdouble getTileLodPitchThreshold(JNIEnv&); + + void setTileLodZoomShift(JNIEnv&, jni::jdouble); + + jni::jdouble getTileLodZoomShift(JNIEnv&); + mbgl::Map& getMap(); void triggerRepaint(JNIEnv&); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java index ab758b79a..cdbecdfd6 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java @@ -358,6 +358,101 @@ public final class MapLibreMap { return nativeMapView.getTileCacheEnabled(); } + /** + * Camera based tile level of detail controls + * + * @param radius minimum radius around the view point in unit of tiles in which the fine + * grained zoom level tiles are always used when performing LOD + * radius must be greater than 1 (At least 1 fine detailed tile is present) + * A smaller radius value may improve performance at the cost of quality (tiles away from + * camera use lower Zoom levels) + */ + public void setTileLodMinRadius(@FloatRange(from = 1, fromInclusive = true) double radius) { + nativeMapView.setTileLodMinRadius(radius); + } + + /** + * Camera based tile level of detail controls + * + * @return minimum radius around the view point in unit of tiles in which the fine grained + * zoom level tiles are always used when performing LOD + * @see MapLibreMap#setTileLodMinRadius(double) + */ + public double getTileLodMinRadius() { + return nativeMapView.getTileLodMinRadius(); + } + + /** + * Camera based tile level of detail controls + * + * @param scale factor for the distance to the camera view point + * A value larger than 1 increases the distance to the camera view point reducing LOD + * Larger values may improve performance at the cost of quality (tiles away from camera + * use lower Zoom levels) + */ + public void setTileLodScale(@FloatRange(from = 0, fromInclusive = false) double scale) { + nativeMapView.setTileLodScale(scale); + } + + /** + * Camera based tile level of detail controls + * + * @return scale factor for the distance to the camera view point + * @see MapLibreMap#setTileLodScale(double) + */ + public double getTileLodScale() { + return nativeMapView.getTileLodScale(); + } + + /** + * Camera based tile level of detail controls + * + * @param threshold pitch angle in radians above which LOD calculation is performed + * A smaller radius value may improve performance at the cost of quality + */ + public void setTileLodPitchThreshold(@FloatRange(from = 0, to = Math.PI) double threshold) { + nativeMapView.setTileLodPitchThreshold(threshold); + } + + /** + * Camera based tile level of detail controls + * + * @return pitch angle threshold in radians above which LOD calculation is performed + * @see MapLibreMap#setTileLodPitchThreshold(double) + */ + public double getTileLodPitchThreshold() { + return nativeMapView.getTileLodPitchThreshold(); + } + + /** + * Camera based tile level of detail controls + * + * @param shift shift applied to the Zoom level during LOD calculation + * A negative value shifts the Zoom level to a coarser level reducing quality but + * improving performance + * A positive value shifts the Zoom level to a finer level increasing details but + * negatively affecting performance + * A value of zero (default) does not apply any shift to the Zoom level + * It is not recommended to change the default value unless performance is critical + * and the loss of quality is acceptable. A value of -1 reduces the number of + * displayed tiles by a factor of 4 on average + * It is recommended to first configure the pixelRatio before adjusting + * TileLodZoomShift. {@link MapLibreMapOptions#pixelRatio(float)} + */ + public void setTileLodZoomShift(double shift) { + nativeMapView.setTileLodZoomShift(shift); + } + + /** + * Camera based tile level of detail controls + * + * @return shift applied to the Zoom level during LOD calculation + * @see MapLibreMap#setTileLodZoomShift(double) + */ + public double getTileLodZoomShift() { + return nativeMapView.getTileLodZoomShift(); + } + // // MinZoom // diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java index 0b3265156..585f7ed33 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java @@ -244,6 +244,22 @@ interface NativeMap { boolean getTileCacheEnabled(); + void setTileLodMinRadius(double radius); + + double getTileLodMinRadius(); + + void setTileLodScale(double scale); + + double getTileLodScale(); + + void setTileLodPitchThreshold(double threshold); + + double getTileLodPitchThreshold(); + + void setTileLodZoomShift(double shift); + + double getTileLodZoomShift(); + void setGestureInProgress(boolean inProgress); float getPixelRatio(); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java index a0ed4dfcc..571193bd4 100755 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java @@ -855,6 +855,70 @@ final class NativeMapView implements NativeMap { } return nativeGetTileCacheEnabled(); } + + @Override + public void setTileLodMinRadius(double radius) { + if (checkState("setTileLodMinRadius")) { + return; + } + nativeSetTileLodMinRadius(radius); + } + + @Override + public double getTileLodMinRadius() { + if (checkState("getTileLodMinRadius")) { + return 0; + } + return nativeGetTileLodMinRadius(); + } + + @Override + public void setTileLodScale(double scale) { + if (checkState("setTileLodScale")) { + return; + } + nativeSetTileLodScale(scale); + } + + @Override + public double getTileLodScale() { + if (checkState("getTileLodScale")) { + return 0; + } + return nativeGetTileLodScale(); + } + + @Override + public void setTileLodPitchThreshold(double threshold) { + if (checkState("setTileLodPitchThreshold")) { + return; + } + nativeSetTileLodPitchThreshold(threshold); + } + + @Override + public double getTileLodPitchThreshold() { + if (checkState("getTileLodPitchThreshold")) { + return 0; + } + return nativeGetTileLodPitchThreshold(); + } + + @Override + public void setTileLodZoomShift(double shift) { + if (checkState("setTileLodZoomShift")) { + return; + } + nativeSetTileLodZoomShift(shift); + } + + @Override + public double getTileLodZoomShift() { + if (checkState("getTileLodZoomShift")) { + return 0; + } + return nativeGetTileLodZoomShift(); + } // Runtime style Api @Override @@ -1644,6 +1708,30 @@ final class NativeMapView implements NativeMap { @Keep private native int nativeGetPrefetchZoomDelta(); + @Keep + private native void nativeSetTileLodMinRadius(double radius); + + @Keep + private native double nativeGetTileLodMinRadius(); + + @Keep + private native void nativeSetTileLodScale(double scale); + + @Keep + private native double nativeGetTileLodScale(); + + @Keep + private native void nativeSetTileLodPitchThreshold(double threshold); + + @Keep + private native double nativeGetTileLodPitchThreshold(); + + @Keep + private native void nativeSetTileLodZoomShift(double shift); + + @Keep + private native double nativeGetTileLodZoomShift(); + @Override public long getNativePtr() { return nativePtr; diff --git a/platform/glfw/glfw_view.cpp b/platform/glfw/glfw_view.cpp index 9ba9a1f76..3485ef5b4 100644 --- a/platform/glfw/glfw_view.cpp +++ b/platform/glfw/glfw_view.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -68,7 +69,6 @@ using namespace std::numbers; #ifdef ENABLE_LOCATION_INDICATOR - namespace { const std::string mbglPuckAssetsPath{MLN_ASSETS_PATH}; @@ -83,7 +83,7 @@ std::array toArray(const mbgl::LatLng &crd) { return {crd.latitude(), crd.longitude(), 0}; } } // namespace -#endif +#endif // ENABLE_LOCATION_INDICATOR class SnapshotObserver final : public mbgl::MapSnapshotterObserver { public: @@ -99,6 +99,76 @@ public: }; namespace { + +enum class TileLodMode { + Default, // Default Tile LOD parameters + NoLod, // Disable LOD + Reduced, // Reduce LOD away from camera + Aggressive, // Aggressively reduce LOD away from camera at the detriment of quality +}; + +constexpr TileLodMode nextTileLodMode(TileLodMode current) { + switch (current) { + case TileLodMode::Default: + return TileLodMode::NoLod; + case TileLodMode::NoLod: + return TileLodMode::Reduced; + case TileLodMode::Reduced: + return TileLodMode::Aggressive; + case TileLodMode::Aggressive: + return TileLodMode::Default; + default: + return TileLodMode::Default; + } +} + +void cycleTileLodMode(mbgl::Map &map) { + // TileLodMode::Default parameters + static const auto defaultRadius = map.getTileLodMinRadius(); + static const auto defaultScale = map.getTileLodScale(); + static const auto defaultTilePitchThreshold = map.getTileLodPitchThreshold(); + + static TileLodMode mode = TileLodMode::Default; + mode = nextTileLodMode(mode); + + switch (mode) { + case TileLodMode::Default: + map.setTileLodMinRadius(defaultRadius); + map.setTileLodScale(defaultScale); + map.setTileLodPitchThreshold(defaultTilePitchThreshold); + mbgl::Log::Info(mbgl::Event::General, "Tile LOD mode: default"); + break; + case TileLodMode::NoLod: + // When LOD is off we set a maximum PitchThreshold + map.setTileLodPitchThreshold(std::numbers::pi); + mbgl::Log::Info(mbgl::Event::General, "Tile LOD mode: disabled"); + break; + case TileLodMode::Reduced: + map.setTileLodMinRadius(2); + map.setTileLodScale(1.5); + map.setTileLodPitchThreshold(std::numbers::pi / 4); + mbgl::Log::Info(mbgl::Event::General, "Tile LOD mode: reduced"); + break; + case TileLodMode::Aggressive: + map.setTileLodMinRadius(1); + map.setTileLodScale(2); + map.setTileLodPitchThreshold(0); + mbgl::Log::Info(mbgl::Event::General, "Tile LOD mode: aggressive"); + break; + } + map.triggerRepaint(); +} + +void tileLodZoomShift(mbgl::Map &map, bool positive) { + constexpr auto tileLodZoomShiftStep = 0.25; + auto shift = positive ? tileLodZoomShiftStep : -tileLodZoomShiftStep; + shift = map.getTileLodZoomShift() + shift; + shift = mbgl::util::clamp(shift, -2.5, 2.5); + mbgl::Log::Info(mbgl::Event::OpenGL, "Zoom shift: " + std::to_string(shift)); + map.setTileLodZoomShift(shift); + map.triggerRepaint(); +} + void addFillExtrusionLayer(mbgl::style::Style &style, bool visible) { MLN_TRACE_FUNC(); @@ -288,6 +358,9 @@ GLFWView::GLFWView(bool fullscreen_, printf("- Press `F1` to generate a render test for the current view\n"); printf("\n"); printf("- Press `Tab` to cycle through the map debug options\n"); + printf("- Press `F6` to cycle through Tile LOD modes\n"); + printf("- Press `F7` to lower the zoom level without changing the camera\n"); + printf("- Press `F8` to higher the zoom level without changing the camera\n"); printf("- Press `Esc` to quit\n"); printf("\n"); printf( @@ -567,6 +640,15 @@ void GLFWView::onKey(GLFWwindow *window, int key, int /*scancode*/, int action, view->freeCameraDemoStartTime = mbgl::Clock::now(); view->invalidate(); } break; + case GLFW_KEY_F6: { + cycleTileLodMode(*view->map); + } break; + case GLFW_KEY_F7: { + tileLodZoomShift(*view->map, false); + } break; + case GLFW_KEY_F8: { + tileLodZoomShift(*view->map, true); + } break; } } diff --git a/platform/ios/app/MBXViewController.mm b/platform/ios/app/MBXViewController.mm index c8bad1e55..b219244cd 100644 --- a/platform/ios/app/MBXViewController.mm +++ b/platform/ios/app/MBXViewController.mm @@ -121,10 +121,19 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) { MBXSettingsMiscellaneousShowCustomLocationManager, MBXSettingsMiscellaneousOrnamentsPlacement, MBXSettingsMiscellaneousLatLngBoundsWithPadding, + MBXSettingsMiscellaneousCycleTileLOD, MBXSettingsMiscellaneousPrintLogFile, MBXSettingsMiscellaneousDeleteLogFile }; +typedef NS_ENUM(NSInteger, MBXTileLodMode) { + MBXTileLodModeNoLod = 0, + MBXTileLodModeDefault, + MBXTileLodModeReduced, + MBXTileLodModeAggressive, + MBXTileLodModeCount +}; + // Utility methods CLLocationCoordinate2D coordinateCentered(CLLocationCoordinate2D origin, CLLocationDegrees bearing, CLLocationDistance distance) { @@ -245,6 +254,8 @@ CLLocationCoordinate2D randomWorldCoordinate(void) { NSDictionary* _pointFeatures; NSLock* _loadLock; + + MBXTileLodMode _tileLodMode; } // MARK: - Setup & Teardown @@ -474,7 +485,8 @@ CLLocationCoordinate2D randomWorldCoordinate(void) { [NSString stringWithFormat:@"Turn %@ Content Insets", (_contentInsetsEnabled ? @"Off" : @"On")], @"View Route Simulation", @"Ornaments Placement", - @"Lat Long bounds with padding" + @"Lat Long bounds with padding", + [NSString stringWithFormat:@"Cycle tile LOD mode: %@", [self getTileLodModeName]] ]]; break; @@ -807,6 +819,9 @@ CLLocationCoordinate2D randomWorldCoordinate(void) { case MBXSettingsMiscellaneousLatLngBoundsWithPadding: [self flyToWithLatLngBoundsAndPadding]; break; + case MBXSettingsMiscellaneousCycleTileLOD: + [self cycleTileLodMode]; + break; default: NSAssert(NO, @"All miscellaneous setting rows should be implemented"); break; @@ -1992,6 +2007,114 @@ CLLocationCoordinate2D randomWorldCoordinate(void) { }]; } +-(void)cycleTileLodMode +{ + // TileLodMode::Default parameters + static const double defaultRadius = self.mapView.tileLodMinRadius; + static const double defaultScale = self.mapView.tileLodScale; + static const double defaultPitchThreshold = self.mapView.tileLodPitchThreshold; + + _tileLodMode = static_cast((static_cast(_tileLodMode) + 1) % static_cast(MBXTileLodModeCount)); + + switch (_tileLodMode) { + case MBXTileLodModeDefault: + self.mapView.tileLodMinRadius = defaultRadius; + self.mapView.tileLodScale = defaultScale; + self.mapView.tileLodPitchThreshold = defaultPitchThreshold; + break; + case MBXTileLodModeNoLod: + // When LOD is off we set a maximum PitchThreshold + self.mapView.tileLodPitchThreshold = M_PI; + break; + case MBXTileLodModeReduced: + self.mapView.tileLodMinRadius = 2; + self.mapView.tileLodScale = 1.5; + self.mapView.tileLodPitchThreshold = M_PI / 4; + break; + case MBXTileLodModeAggressive: + self.mapView.tileLodMinRadius = 1; + self.mapView.tileLodScale = 2; + self.mapView.tileLodPitchThreshold = 0; + break; + default: + break; + } + + // update UI + static UISlider* zoomSlider = nil; + if (zoomSlider && _tileLodMode == MBXTileLodModeNoLod) { + [zoomSlider removeFromSuperview]; + zoomSlider = nil; + } + + if (!zoomSlider && _tileLodMode != MBXTileLodModeNoLod) { + const float zoomShiftRange = 5.0f; + zoomSlider = [[UISlider alloc] init]; + zoomSlider.minimumValue = -zoomShiftRange / 2.0f; + zoomSlider.maximumValue = zoomShiftRange / 2.0f; + zoomSlider.value = self.mapView.tileLodZoomShift; + zoomSlider.continuous = NO; + + [zoomSlider addTarget:self action:@selector(updateTileLodZoom:) forControlEvents:UIControlEventValueChanged]; + [self.view addSubview:zoomSlider]; + + zoomSlider.translatesAutoresizingMaskIntoConstraints = NO; + zoomSlider.frame = CGRectMake(0, 0, 100, 100); + + NSArray* layout = @[ + [NSLayoutConstraint constraintWithItem:zoomSlider + attribute:NSLayoutAttributeCenterX + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeCenterX + multiplier:1 + constant:0], + [NSLayoutConstraint constraintWithItem:zoomSlider + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeWidth + multiplier:0.5 + constant:0], + [NSLayoutConstraint constraintWithItem:zoomSlider + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeCenterY + multiplier:1 + constant:0], + [zoomSlider.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor]]; + + [self.view addConstraints:layout]; + } + + [self.mapView triggerRepaint]; +} + +-(void)updateTileLodZoom:(id)sender { + UISlider* slider = (UISlider*)sender; + + self.mapView.tileLodZoomShift = slider.value; + [self.mapView triggerRepaint]; + + NSLog(@"Tile LOD zoom shift: %f", self.mapView.tileLodZoomShift); +} + +-(NSString*)getTileLodModeName { + switch (_tileLodMode) { + case MBXTileLodModeDefault: + return @"Default"; + case MBXTileLodModeNoLod: + return @"Disabled"; + case MBXTileLodModeReduced: + return @"Reduced"; + case MBXTileLodModeAggressive: + return @"Aggressive"; + default: + return @""; + } +} + // MARK: - Random World Tour - (void)addAnnotations:(NSInteger)numAnnotations aroundCoordinate:(CLLocationCoordinate2D)coordinate radius:(CLLocationDistance)radius { diff --git a/platform/ios/src/MLNMapView.h b/platform/ios/src/MLNMapView.h index 9308ded6e..f8455efe3 100644 --- a/platform/ios/src/MLNMapView.h +++ b/platform/ios/src/MLNMapView.h @@ -463,6 +463,50 @@ MLN_EXPORT @property (nonatomic, assign) BOOL tileCacheEnabled; +// MARK: Tile LOD controls + +/** + Camera based tile level of detail controls + + Minimum radius around the view point in unit of tiles in which the fine + grained zoom level tiles are always used when performing LOD + radius must be greater than 1 (At least 1 fine detailed tile is present) + A smaller radius value may improve performance at the cost of quality (tiles away from + camera use lower Zoom levels) + */ +@property (nonatomic, assign) double tileLodMinRadius; + +/** + Camera based tile level of detail controls + + Factor for the distance to the camera view point + A value larger than 1 increases the distance to the camera view point reducing LOD + Larger values may improve performance at the cost of quality (tiles away from camera + use lower Zoom levels) + */ +@property (nonatomic, assign) double tileLodScale; + +/** + Camera based tile level of detail controls + + Pitch angle in radians above which LOD calculation is performed + A smaller radius value may improve performance at the cost of quality + */ +@property (nonatomic, assign) double tileLodPitchThreshold; + +/** + Camera based tile level of detail controls + + Shift applied to the Zoom level during LOD calculation + A negative value shifts the Zoom level to a coarser level reducing quality but improving + performance A positive value shifts the Zoom level to a finer level increasing details but + negatively affecting performance A value of zero (default) does not apply any shift to the Zoom + level It is not recommended to change the default value unless performance is critical and the loss + of quality is acceptable. A value of -1 reduces the number of displayed tiles by a factor of 4 on + average It is recommended to first configure the pixelRatio before adjusting TileLodZoomShift. + */ +@property (nonatomic, assign) double tileLodZoomShift; + // MARK: Displaying the User’s Location /** diff --git a/platform/ios/src/MLNMapView.mm b/platform/ios/src/MLNMapView.mm index fcb081113..56de8dc12 100644 --- a/platform/ios/src/MLNMapView.mm +++ b/platform/ios/src/MLNMapView.mm @@ -3231,6 +3231,46 @@ static void *windowScreenContext = &windowScreenContext; return _rendererFrontend->getTileCacheEnabled(); } +- (void)setTileLodMinRadius:(double)tileLodMinRadius +{ + _mbglMap->setTileLodMinRadius(tileLodMinRadius); +} + +- (double)tileLodMinRadius +{ + return _mbglMap->getTileLodMinRadius(); +} + +- (void)setTileLodScale:(double)tileLodScale +{ + _mbglMap->setTileLodScale(tileLodScale); +} + +- (double)tileLodScale +{ + return _mbglMap->getTileLodScale(); +} + +-(void)setTileLodPitchThreshold:(double)tileLodPitchThreshold +{ + _mbglMap->setTileLodPitchThreshold(tileLodPitchThreshold); +} + +-(double)tileLodPitchThreshold +{ + return _mbglMap->getTileLodPitchThreshold(); +} + +-(void)setTileLodZoomShift:(double)tileLodZoomShift +{ + _mbglMap->setTileLodZoomShift(tileLodZoomShift); +} + +-(double)tileLodZoomShift +{ + return _mbglMap->getTileLodZoomShift(); +} + // MARK: - Accessibility - - (NSString *)accessibilityValue diff --git a/platform/macos/src/MLNMapView.h b/platform/macos/src/MLNMapView.h index ef4ac5e12..389474131 100644 --- a/platform/macos/src/MLNMapView.h +++ b/platform/macos/src/MLNMapView.h @@ -202,6 +202,50 @@ MLN_EXPORT IB_DESIGNABLE @interface MLNMapView : NSView */ @property (nonatomic, assign) BOOL prefetchesTiles; +// MARK: Tile LOD controls + +/** + Camera based tile level of detail controls + + Minimum radius around the view point in unit of tiles in which the fine + grained zoom level tiles are always used when performing LOD + radius must be greater than 1 (At least 1 fine detailed tile is present) + A smaller radius value may improve performance at the cost of quality (tiles away from + camera use lower Zoom levels) + */ +@property (nonatomic, assign) double tileLodMinRadius; + +/** + Camera based tile level of detail controls + + Factor for the distance to the camera view point + A value larger than 1 increases the distance to the camera view point reducing LOD + Larger values may improve performance at the cost of quality (tiles away from camera + use lower Zoom levels) + */ +@property (nonatomic, assign) double tileLodScale; + +/** + Camera based tile level of detail controls + + Pitch angle in radians above which LOD calculation is performed + A smaller radius value may improve performance at the cost of quality + */ +@property (nonatomic, assign) double tileLodPitchThreshold; + +/** + Camera based tile level of detail controls + + Shift applied to the Zoom level during LOD calculation + A negative value shifts the Zoom level to a coarser level reducing quality but improving + performance A positive value shifts the Zoom level to a finer level increasing details but + negatively affecting performance A value of zero (default) does not apply any shift to the Zoom + level It is not recommended to change the default value unless performance is critical and the loss + of quality is acceptable. A value of -1 reduces the number of displayed tiles by a factor of 4 on + average It is recommended to first configure the pixelRatio before adjusting TileLodZoomShift. + */ +@property (nonatomic, assign) double tileLodZoomShift; + // MARK: Manipulating the Viewpoint /** diff --git a/platform/macos/src/MLNMapView.mm b/platform/macos/src/MLNMapView.mm index 83cd35ed9..6dc79ba88 100644 --- a/platform/macos/src/MLNMapView.mm +++ b/platform/macos/src/MLNMapView.mm @@ -710,6 +710,46 @@ public: return _mbglMap->getPrefetchZoomDelta() > 0 ? YES : NO; } +- (void)setTileLodMinRadius:(double)tileLodMinRadius +{ + _mbglMap->setTileLodMinRadius(tileLodMinRadius); +} + +- (double)tileLodMinRadius +{ + return _mbglMap->getTileLodMinRadius(); +} + +- (void)setTileLodScale:(double)tileLodScale +{ + _mbglMap->setTileLodScale(tileLodScale); +} + +- (double)tileLodScale +{ + return _mbglMap->getTileLodScale(); +} + +-(void)setTileLodPitchThreshold:(double)tileLodPitchThreshold +{ + _mbglMap->setTileLodPitchThreshold(tileLodPitchThreshold); +} + +-(double)tileLodPitchThreshold +{ + return _mbglMap->getTileLodPitchThreshold(); +} + +-(void)setTileLodZoomShift:(double)tileLodZoomShift +{ + _mbglMap->setTileLodZoomShift(tileLodZoomShift); +} + +-(double)tileLodZoomShift +{ + return _mbglMap->getTileLodZoomShift(); +} + - (mbgl::Renderer *)renderer { return _rendererFrontend->getRenderer(); } diff --git a/src/mbgl/map/map.cpp b/src/mbgl/map/map.cpp index c298c1242..3f2b3c298 100644 --- a/src/mbgl/map/map.cpp +++ b/src/mbgl/map/map.cpp @@ -547,6 +547,38 @@ FreeCameraOptions Map::getFreeCameraOptions() const { return impl->transform.getFreeCameraOptions(); } +void Map::setTileLodMinRadius(double radius) { + impl->tileLodMinRadius = radius; +} + +double Map::getTileLodMinRadius() const { + return impl->tileLodMinRadius; +} + +void Map::setTileLodScale(double scale) { + impl->tileLodScale = scale; +} + +double Map::getTileLodScale() const { + return impl->tileLodScale; +} + +void Map::setTileLodPitchThreshold(double threshold) { + impl->tileLodPitchThreshold = threshold; +} + +double Map::getTileLodPitchThreshold() const { + return impl->tileLodPitchThreshold; +} + +void Map::setTileLodZoomShift(double shift) { + impl->tileLodZoomShift = shift; +} + +double Map::getTileLodZoomShift() const { + return impl->tileLodZoomShift; +} + ClientOptions Map::getClientOptions() const { return impl->fileSource ? impl->fileSource->getClientOptions() : ClientOptions(); } diff --git a/src/mbgl/map/map_impl.cpp b/src/mbgl/map/map_impl.cpp index 7ef9d8c50..4fd84cd02 100644 --- a/src/mbgl/map/map_impl.cpp +++ b/src/mbgl/map/map_impl.cpp @@ -130,7 +130,11 @@ void Map::Impl::onUpdate() { fileSource, prefetchZoomDelta, bool(stillImageRequest), - crossSourceCollisions}; + crossSourceCollisions, + tileLodMinRadius, + tileLodScale, + tileLodPitchThreshold, + tileLodZoomShift}; rendererFrontend.update(std::make_shared(std::move(params))); } diff --git a/src/mbgl/map/map_impl.hpp b/src/mbgl/map/map_impl.hpp index f280bd1da..1207b510a 100644 --- a/src/mbgl/map/map_impl.hpp +++ b/src/mbgl/map/map_impl.hpp @@ -14,6 +14,8 @@ #include #include +#include + namespace mbgl { class FileSource; @@ -104,6 +106,11 @@ public: bool loading = false; bool rendererFullyLoaded; std::unique_ptr stillImageRequest; + + double tileLodMinRadius = 3; + double tileLodScale = 1; + double tileLodPitchThreshold = (60.0 / 180.0) * std::numbers::pi; + double tileLodZoomShift = 0; }; // Forward declaration of this method is required for the MapProjection class diff --git a/src/mbgl/renderer/layers/render_background_layer.cpp b/src/mbgl/renderer/layers/render_background_layer.cpp index bc61344b4..0e18fec44 100644 --- a/src/mbgl/renderer/layers/render_background_layer.cpp +++ b/src/mbgl/renderer/layers/render_background_layer.cpp @@ -113,11 +113,16 @@ static constexpr std::string_view BackgroundPatternShaderName = "BackgroundPatte void RenderBackgroundLayer::update(gfx::ShaderRegistry& shaders, gfx::Context& context, const TransformState& state, - const std::shared_ptr&, + const std::shared_ptr& updateParameters, [[maybe_unused]] const RenderTree& renderTree, [[maybe_unused]] UniqueChangeRequestVec& changes) { + assert(updateParameters); const auto zoom = state.getIntegerZoom(); - const auto tileCover = util::tileCover(state, zoom); + const auto tileCover = util::tileCover({state, + updateParameters->tileLodMinRadius, + updateParameters->tileLodScale, + updateParameters->tileLodPitchThreshold}, + zoom); // renderTiles is always empty, we use tileCover instead if (tileCover.empty()) { diff --git a/src/mbgl/renderer/paint_parameters.cpp b/src/mbgl/renderer/paint_parameters.cpp index 5e72b914f..1b80741d3 100644 --- a/src/mbgl/renderer/paint_parameters.cpp +++ b/src/mbgl/renderer/paint_parameters.cpp @@ -55,7 +55,10 @@ PaintParameters::PaintParameters(gfx::Context& context_, RenderStaticData& staticData_, LineAtlas& lineAtlas_, PatternAtlas& patternAtlas_, - uint64_t frameCount_) + uint64_t frameCount_, + double tileLodMinRadius_, + double tileLodScale_, + double tileLodPitchThreshold_) : context(context_), backend(backend_), encoder(context.createCommandEncoder()), @@ -70,7 +73,10 @@ PaintParameters::PaintParameters(gfx::Context& context_, timePoint(timePoint_), pixelRatio(pixelRatio_), shaders(*staticData_.shaders), - frameCount(frameCount_) { + frameCount(frameCount_), + tileLodMinRadius(tileLodMinRadius_), + tileLodScale(tileLodScale_), + tileLodPitchThreshold(tileLodPitchThreshold_) { pixelsToGLUnits = {{2.0f / state.getSize().width, -2.0f / state.getSize().height}}; if (state.getViewportMode() == ViewportMode::FlippedY) { diff --git a/src/mbgl/renderer/paint_parameters.hpp b/src/mbgl/renderer/paint_parameters.hpp index 545bdaf0b..4f4be3b2c 100644 --- a/src/mbgl/renderer/paint_parameters.hpp +++ b/src/mbgl/renderer/paint_parameters.hpp @@ -57,7 +57,10 @@ public: RenderStaticData&, LineAtlas&, PatternAtlas&, - uint64_t frameCount); + uint64_t frameCount, + double tileLodMinRadius, + double tileLodScale, + double tileLodPitchThreshold); ~PaintParameters(); gfx::Context& context; @@ -130,6 +133,10 @@ public: static constexpr float depthEpsilon = 1.0f / (1 << 12); #endif static constexpr int maxStencilValue = 255; + + double tileLodMinRadius; + double tileLodScale; + double tileLodPitchThreshold; }; } // namespace mbgl diff --git a/src/mbgl/renderer/render_orchestrator.cpp b/src/mbgl/renderer/render_orchestrator.cpp index b625617a8..8f6e3abf0 100644 --- a/src/mbgl/renderer/render_orchestrator.cpp +++ b/src/mbgl/renderer/render_orchestrator.cpp @@ -194,6 +194,10 @@ std::unique_ptr RenderOrchestrator::createRenderTree( .glyphManager = glyphManager, .prefetchZoomDelta = updateParameters->prefetchZoomDelta, .threadPool = threadPool, + .tileLodMinRadius = updateParameters->tileLodMinRadius, + .tileLodScale = updateParameters->tileLodScale, + .tileLodPitchThreshold = updateParameters->tileLodPitchThreshold, + .tileLodZoomShift = updateParameters->tileLodZoomShift, .dynamicTextureAtlas = dynamicTextureAtlas}; glyphManager->setURL(updateParameters->glyphURL); diff --git a/src/mbgl/renderer/renderer_impl.cpp b/src/mbgl/renderer/renderer_impl.cpp index 3e5a64208..50dc45910 100644 --- a/src/mbgl/renderer/renderer_impl.cpp +++ b/src/mbgl/renderer/renderer_impl.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -84,12 +85,13 @@ void Renderer::Impl::setObserver(RendererObserver* observer_) { observer = observer_ ? observer_ : &nullObserver(); } -void Renderer::Impl::render(const RenderTree& renderTree, - [[maybe_unused]] const std::shared_ptr& updateParameters) { +void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr& updateParameters) { MLN_TRACE_FUNC(); auto& context = backend.getContext(); context.setObserver(this); + assert(updateParameters); + #if MLN_RENDER_BACKEND_METAL if constexpr (EnableMetalCapture) { const auto& mtlBackend = static_cast(backend); @@ -189,7 +191,10 @@ void Renderer::Impl::render(const RenderTree& renderTree, *staticData, renderTree.getLineAtlas(), renderTree.getPatternAtlas(), - frameCount}; + frameCount, + updateParameters->tileLodMinRadius, + updateParameters->tileLodScale, + updateParameters->tileLodPitchThreshold}; parameters.symbolFadeChange = renderTreeParameters.symbolFadeChange; parameters.opaquePassCutoff = renderTreeParameters.opaquePassCutOff; diff --git a/src/mbgl/renderer/sources/render_image_source.cpp b/src/mbgl/renderer/sources/render_image_source.cpp index d0bdb3d87..e88a806bb 100644 --- a/src/mbgl/renderer/sources/render_image_source.cpp +++ b/src/mbgl/renderer/sources/render_image_source.cpp @@ -133,7 +133,9 @@ void RenderImageSource::update(Immutable baseImpl_, bool hasVisibleTile = false; // Add additional wrapped tile ids if neccessary - auto idealTiles = util::tileCover(transformState, static_cast(transformState.getZoom())); + auto idealTiles = util::tileCover( + {transformState, parameters.tileLodMinRadius, parameters.tileLodScale, parameters.tileLodPitchThreshold}, + static_cast(transformState.getZoom())); for (auto tile : idealTiles) { if (tile.wrap != 0 && tileCover[0].canonical.isChildOf(tile.canonical)) { tileIds.emplace_back(tile.wrap, tileCover[0].canonical); diff --git a/src/mbgl/renderer/tile_parameters.hpp b/src/mbgl/renderer/tile_parameters.hpp index 0596fcce8..32e3911c9 100644 --- a/src/mbgl/renderer/tile_parameters.hpp +++ b/src/mbgl/renderer/tile_parameters.hpp @@ -4,6 +4,7 @@ #include #include +#include #include @@ -32,6 +33,10 @@ public: std::shared_ptr glyphManager; const uint8_t prefetchZoomDelta; TaggedScheduler threadPool; + double tileLodMinRadius = 3; + double tileLodScale = 1; + double tileLodPitchThreshold = (60.0 / 180.0) * std::numbers::pi; + double tileLodZoomShift = 0; gfx::DynamicTextureAtlasPtr dynamicTextureAtlas; }; diff --git a/src/mbgl/renderer/tile_pyramid.cpp b/src/mbgl/renderer/tile_pyramid.cpp index 3e79657b5..2c62ec348 100644 --- a/src/mbgl/renderer/tile_pyramid.cpp +++ b/src/mbgl/renderer/tile_pyramid.cpp @@ -89,9 +89,14 @@ void TilePyramid::update(const std::vector>& l handleWrapJump(static_cast(parameters.transformState.getLatLng().longitude())); + // Optionally shift the zoom level + double zoom = util::clamp(parameters.transformState.getZoom() + parameters.tileLodZoomShift, + parameters.transformState.getMinZoom(), + parameters.transformState.getMaxZoom()); + const auto type = sourceImpl.type; // Determine the overzooming/underzooming amounts and required tiles. - int32_t overscaledZoom = util::coveringZoomLevel(parameters.transformState.getZoom(), type, tileSize); + int32_t overscaledZoom = util::coveringZoomLevel(zoom, type, tileSize); int32_t tileZoom = overscaledZoom; int32_t panZoom = zoomRange.max; @@ -103,6 +108,11 @@ void TilePyramid::update(const std::vector>& l std::vector idealTiles; std::vector panTiles; + util::TileCoverParameters tileCoverParameters = {parameters.transformState, + parameters.tileLodMinRadius, + parameters.tileLodScale, + parameters.tileLodPitchThreshold}; + if (overscaledZoom >= zoomRange.min) { int32_t idealZoom = std::min(zoomRange.max, overscaledZoom); @@ -123,11 +133,11 @@ void TilePyramid::update(const std::vector>& l } if (panZoom < idealZoom) { - panTiles = util::tileCover(parameters.transformState, panZoom); + panTiles = util::tileCover(tileCoverParameters, panZoom); } } - idealTiles = util::tileCover(parameters.transformState, idealZoom, tileZoom); + idealTiles = util::tileCover(tileCoverParameters, idealZoom, tileZoom); if (parameters.mode == MapMode::Tile && type != SourceType::Raster && type != SourceType::RasterDEM && idealTiles.size() > 1) { mbgl::Log::Warning(mbgl::Event::General, diff --git a/src/mbgl/renderer/update_parameters.hpp b/src/mbgl/renderer/update_parameters.hpp index a16a2801f..d2b162c29 100644 --- a/src/mbgl/renderer/update_parameters.hpp +++ b/src/mbgl/renderer/update_parameters.hpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -44,6 +45,11 @@ public: const bool stillImageRequest; const bool crossSourceCollisions; + + double tileLodMinRadius = 3; + double tileLodScale = 1; + double tileLodPitchThreshold = (60.0 / 180.0) * std::numbers::pi; + double tileLodZoomShift = 0; }; } // namespace mbgl diff --git a/src/mbgl/util/tile_cover.cpp b/src/mbgl/util/tile_cover.cpp index 1a3055788..d4f2e4eab 100644 --- a/src/mbgl/util/tile_cover.cpp +++ b/src/mbgl/util/tile_cover.cpp @@ -1,4 +1,3 @@ -#include #include #include #include @@ -9,7 +8,6 @@ #include #include -#include using namespace std::numbers; @@ -157,7 +155,7 @@ int32_t coveringZoomLevel(double zoom, style::SourceType type, uint16_t size) no } } -std::vector tileCover(const TransformState& state, +std::vector tileCover(const TileCoverParameters& state, uint8_t z, const std::optional& overscaledZ) { struct Node { @@ -173,23 +171,26 @@ std::vector tileCover(const TransformState& state, double sqrDist; }; + const auto& transform = state.transformState; const double numTiles = std::pow(2.0, z); - const double worldSize = Projection::worldSize(state.getScale()); - const uint8_t minZoom = state.getPitch() <= (60.0 / 180.0) * pi ? z : 0; + const double worldSize = Projection::worldSize(transform.getScale()); + const uint8_t minZoom = transform.getPitch() <= state.tileLodPitchThreshold ? z : 0; const uint8_t maxZoom = z; - const uint8_t overscaledZoom = overscaledZ.value_or(z); - const bool flippedY = state.getViewportMode() == ViewportMode::FlippedY; + const uint8_t overscaledZoom = std::max(overscaledZ.value_or(z), z); + const bool flippedY = transform.getViewportMode() == ViewportMode::FlippedY; - const auto centerPoint = - TileCoordinate::fromScreenCoordinate(state, z, {state.getSize().width / 2.0, state.getSize().height / 2.0}).p; + const auto centerPoint = TileCoordinate::fromScreenCoordinate( + transform, z, {transform.getSize().width / 2.0, transform.getSize().height / 2.0}) + .p; const vec3 centerCoord = {{centerPoint.x, centerPoint.y, 0.0}}; - const Frustum frustum = Frustum::fromInvProjMatrix(state.getInvProjectionMatrix(), worldSize, z, flippedY); + const Frustum frustum = Frustum::fromInvProjMatrix(transform.getInvProjectionMatrix(), worldSize, z, flippedY); // There should always be a certain number of maximum zoom level tiles // surrounding the center location - const double radiusOfMaxLvlLodInTiles = 3; + assert(state.tileLodMinRadius >= 1); + const double radiusOfMaxLvlLodInTiles = std::max(1.0, state.tileLodMinRadius); const auto newRootTile = [&](int16_t wrap) -> Node { return {AABB({{wrap * numTiles, 0.0, 0.0}}, {{(wrap + 1) * numTiles, numTiles, 0.0}}), @@ -235,12 +236,14 @@ std::vector tileCover(const TransformState& state, // there's always a certain number of maxLevel tiles next to the map // center. Using the fact that a parent node in quadtree is twice the // size of its children (per dimension) we can define distance - // thresholds for each relative level: f(k) = offset + 2 + 4 + 8 + 16 + - // ... + 2^k. This is the same as "offset+2^(k+1)-2" + // thresholds for each relative level: + // f(k) = offset + 2 + 4 + 8 + 16 + ... + 2^k + // This is the same as: + // f(k) = offset + 2^(k+1)-2 const double distToSplit = radiusOfMaxLvlLodInTiles + (1 << (maxZoom - node.zoom)) - 2; // Have we reached the target depth or is the tile too far away to be any split further? - if (node.zoom == maxZoom || (*longestDim > distToSplit && node.zoom >= minZoom)) { + if (node.zoom == maxZoom || (*longestDim * state.tileLodScale > distToSplit && node.zoom >= minZoom)) { // Perform precise intersection test between the frustum and aabb. // This will cull < 1% false positives missed by the original test if (node.fullyVisible || frustum.intersectsPrecise(node.aabb, true) != IntersectionResult::Separate) { diff --git a/src/mbgl/util/tile_cover.hpp b/src/mbgl/util/tile_cover.hpp index 972fdc5ed..a935b2d3a 100644 --- a/src/mbgl/util/tile_cover.hpp +++ b/src/mbgl/util/tile_cover.hpp @@ -1,16 +1,18 @@ #pragma once +#include #include #include #include +#include #include #include +#include #include namespace mbgl { -class TransformState; class LatLngBounds; namespace util { @@ -31,9 +33,16 @@ private: std::unique_ptr impl; }; +struct TileCoverParameters { + TransformState transformState; + double tileLodMinRadius = 3; + double tileLodScale = 1; + double tileLodPitchThreshold = (60.0 / 180.0) * std::numbers::pi; +}; + int32_t coveringZoomLevel(double z, style::SourceType type, uint16_t tileSize) noexcept; -std::vector tileCover(const TransformState&, +std::vector tileCover(const TileCoverParameters& state, uint8_t z, const std::optional& overscaledZ = std::nullopt); std::vector tileCover(const LatLngBounds&, uint8_t z); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 60fc46969..940c1e658 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -84,6 +84,7 @@ add_library( ${PROJECT_SOURCE_DIR}/test/tile/tile_cache.test.cpp ${PROJECT_SOURCE_DIR}/test/tile/tile_coordinate.test.cpp ${PROJECT_SOURCE_DIR}/test/tile/tile_id.test.cpp + ${PROJECT_SOURCE_DIR}/test/tile/tile_lod.test.cpp ${PROJECT_SOURCE_DIR}/test/tile/vector_tile.test.cpp ${PROJECT_SOURCE_DIR}/test/util/action_journal.test.cpp ${PROJECT_SOURCE_DIR}/test/util/async_task.test.cpp diff --git a/test/fixtures/tile_lod/disabled/expected.png b/test/fixtures/tile_lod/disabled/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..e86d8f954830e5e2903cef025fa4769c3784dd99 GIT binary patch literal 872 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 z zA4@3Fd!K`HHtXR7TRXVV;;g95%goy=qIMQh`fxD7~7XEu^#Yd9LheGPOqaidKJdeVQ@2Q3Q$~!xm z=bIz_oZD&!kK}M=MLbqd1Ef$Y`L2A(mel$FL!A|1G z^NyT6(>RIwrcvd&>{G7gD85Xp4=F6D&xD;Po>PE5K4X$4)m_&D^iooqr$Y~yOD-lN zU`N2}$oTsLhG-)KU#YsjIbkIz4fB~9>e5ipq0lvg<$7o=hDh*_5hgX&v%kY`mA%dx z-YXu_uXLp{OB)8kMXjC0)%$4>f~dq2wRA!$dUlZ`!JjsvlTJikqS5HyjH{&DXcG0# z!1IV^4m=wit#8q2jtuQLONcf2?@X@jB%PAF2YUR7C8AK7FAO(oAS{H()7pKo0k%uX zP7n#pwZJT#X^R(e46Qpkn5f843tIdb+Pm8Q2`xCq9{}N$*#x1(xZ<@@b=+rN`xx3s zx{pz|JvTiD2>o~fV18->L)zsryaqsEA@Bx3;KhLEla6q!!ek}4vk>?h@-6U;B*1X> z)BHE~yJIhGF<`2YTM@#r0p&41FN-TH#uqC`$=9LdJC2mGbkfyr1z(X{`#U4M2mSO4 zB~F#_m@!KJ32G5I(9R4u;_i7ZpfZaMwW!X2lb!J1NxT7(f(B4L*A!swttatJZrYbY zv8cY`#aA8`dVYhm(@dxCQKSx%m#~WNh-6l|9&b*CR+)e>J^CXk(9Rwk5Yl)avR&fCbaWuFVBLAZlm*6pE(`{1mX8io6*K0%LZQ?J zpNqrQvU+2F$>tVy0jN-SztouzG?C1P9Q7O((%iQR_=Tm)fu(gIGkQ3}u_00vvo+Xm3f zvusE<@=)|CJBvi-LJ@ZxOeJj;dzV4L*U>?qnaP(Q{LL&c^)-u`)Ot!2F#k?IF;Q* zG`qoNE!mB7@gHWXztCcLME-uc#xEUyhMWhA#!n`j^G>Mfl)j+e>F0oY#CZA{8ziY& z6?3r3mx5n(ZPZ{oOe-TyKh;HA0e0$Ql~qp853`&?RfZx9VSMfJZk~8+L%;s zvSQ+^6STjmqp9PdGQb*nNe&n~lS)OW}~p7uGtZE5{_nD_z|?fwS2iGF$HfC?@jhCwEL}} zmUS{v94crKEzQ~$(BQmh0skiZ4q(TKg&oPpLEr~RZ7fIjn?yTw8;mK*H1mDQ`UTyy3Y~s?c z3tvYrGtV+X@B#KWx8jd#Fxoe&qT%&U;;i>@RNH9tapOW%=eA3AQ!__^R&-fbYZ#?gR=DilT>=P-0=&)n5$@wNC!nMLS4NR=ihTtth JRe`aY{{_g`gzNwS literal 0 HcmV?d00001 diff --git a/test/fixtures/tile_lod/scale/expected.png b/test/fixtures/tile_lod/scale/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..9050578d4852fb69da0ba009b48fdf72cbb2e27d GIT binary patch literal 3416 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 possibleExpected, + const PremultipliedImage& actual, + double imageThreshold = 0, + double pixelThreshold = 0); + +double getImageDiff(const std::string& base, const PremultipliedImage& actual, double pixelThreshold = 0); + } // namespace test } // namespace mbgl diff --git a/test/include/mbgl/test/vector_tile_test.hpp b/test/include/mbgl/test/vector_tile_test.hpp index 3e2b41d73..9ae85853b 100644 --- a/test/include/mbgl/test/vector_tile_test.hpp +++ b/test/include/mbgl/test/vector_tile_test.hpp @@ -34,17 +34,17 @@ public: VectorTileTest() : threadPool(Scheduler::GetBackground(), uniqueID), - tileParameters{1.0, - MapDebugOptions(), - transformState, - fileSource, - MapMode::Continuous, - annotationManager.makeWeakPtr(), - imageManager, - glyphManager, - 0, - threadPool, - dynamicTextureAtlas}, + tileParameters{.pixelRatio = 1.0, + .debugOptions = MapDebugOptions(), + .transformState = transformState, + .fileSource = fileSource, + .mode = MapMode::Continuous, + .annotationManager = annotationManager.makeWeakPtr(), + .imageManager = imageManager, + .glyphManager = glyphManager, + .prefetchZoomDelta = 0, + .threadPool = threadPool, + .dynamicTextureAtlas = dynamicTextureAtlas}, style{fileSource, 1, threadPool} {} ~VectorTileTest() { diff --git a/test/src/mbgl/test/util.cpp b/test/src/mbgl/test/util.cpp index 79de8bd44..dafb5fb8f 100644 --- a/test/src/mbgl/test/util.cpp +++ b/test/src/mbgl/test/util.cpp @@ -9,12 +9,16 @@ #pragma warning(disable : 4244) #endif +#include +#include #include #ifdef _MSC_VER #pragma warning(pop) #endif +using namespace ::testing; + namespace mbgl { namespace test { @@ -22,10 +26,27 @@ void checkImage(const std::string& base, const PremultipliedImage& actual, double imageThreshold, double pixelThreshold) { + EXPECT_LE(getImageDiff(base, actual, pixelThreshold), imageThreshold); +} + +void checkImages(const std::vector& possibleExpected, + const PremultipliedImage& actual, + double imageThreshold, + double pixelThreshold) { + std::vector diffs(0.0, possibleExpected.size()); + + for (const auto& expected : possibleExpected) { + diffs.push_back(getImageDiff(expected, actual, pixelThreshold)); + } + + EXPECT_THAT(diffs, Contains(Le(imageThreshold))); +} + +double getImageDiff(const std::string& base, const PremultipliedImage& actual, double pixelThreshold) { #if !TEST_READ_ONLY if (getenv("UPDATE")) { util::write_file(base + "/expected.png", encodePNG(actual)); - return; + return 0.0; } #endif @@ -44,7 +65,9 @@ void checkImage(const std::string& base, util::write_file(base + "/actual.png", encodePNG(actual)); #endif - ASSERT_EQ(expected.size, actual.size); + if (expected.size != actual.size) { + return 1.0; + } uint64_t pixels = mapbox::pixelmatch(actual.data.get(), expected.data.get(), @@ -53,11 +76,11 @@ void checkImage(const std::string& base, diff.data.get(), pixelThreshold); - EXPECT_LE(static_cast(pixels) / (expected.size.width * expected.size.height), imageThreshold); - #if !TEST_READ_ONLY util::write_file(base + "/diff.png", encodePNG(diff)); #endif + + return static_cast(pixels) / (expected.size.width * expected.size.height); } } // namespace test diff --git a/test/style/source.test.cpp b/test/style/source.test.cpp index 8aad9033e..70d784111 100644 --- a/test/style/source.test.cpp +++ b/test/style/source.test.cpp @@ -70,17 +70,17 @@ public: Style style; TileParameters tileParameters(MapMode mapMode = MapMode::Continuous) { - return {1.0, - MapDebugOptions(), - transformState, - fileSource, - mapMode, - annotationManager.makeWeakPtr(), - imageManager, - glyphManager, - 0, - threadPool, - dynamicTextureAtlas}; + return {.pixelRatio = 1.0, + .debugOptions = MapDebugOptions(), + .transformState = transformState, + .fileSource = fileSource, + .mode = mapMode, + .annotationManager = annotationManager.makeWeakPtr(), + .imageManager = imageManager, + .glyphManager = glyphManager, + .prefetchZoomDelta = 0, + .threadPool = threadPool, + .dynamicTextureAtlas = dynamicTextureAtlas}; }; SourceTest() diff --git a/test/tile/custom_geometry_tile.test.cpp b/test/tile/custom_geometry_tile.test.cpp index c630ae271..fed5a36ba 100644 --- a/test/tile/custom_geometry_tile.test.cpp +++ b/test/tile/custom_geometry_tile.test.cpp @@ -36,17 +36,17 @@ public: style::Style style; CustomTileTest() - : tileParameters{1.0, - MapDebugOptions(), - transformState, - fileSource, - MapMode::Continuous, - annotationManager.makeWeakPtr(), - imageManager, - glyphManager, - 0, - {Scheduler::GetBackground(), uniqueID}, - dynamicTextureAtlas}, + : tileParameters{.pixelRatio = 1.0, + .debugOptions = MapDebugOptions(), + .transformState = transformState, + .fileSource = fileSource, + .mode = MapMode::Continuous, + .annotationManager = annotationManager.makeWeakPtr(), + .imageManager = imageManager, + .glyphManager = glyphManager, + .prefetchZoomDelta = 0, + .threadPool = {Scheduler::GetBackground(), uniqueID}, + .dynamicTextureAtlas = dynamicTextureAtlas}, style{fileSource, 1, tileParameters.threadPool} {} }; diff --git a/test/tile/geojson_tile.test.cpp b/test/tile/geojson_tile.test.cpp index f1fb4dbf7..33baa2414 100644 --- a/test/tile/geojson_tile.test.cpp +++ b/test/tile/geojson_tile.test.cpp @@ -37,17 +37,17 @@ public: style::Style style; GeoJSONTileTest() - : tileParameters{1.0, - MapDebugOptions(), - transformState, - fileSource, - MapMode::Continuous, - annotationManager.makeWeakPtr(), - imageManager, - glyphManager, - 0, - {Scheduler::GetBackground(), uniqueID}, - dynamicTextureAtlas}, + : tileParameters{.pixelRatio = 1.0, + .debugOptions = MapDebugOptions(), + .transformState = transformState, + .fileSource = fileSource, + .mode = MapMode::Continuous, + .annotationManager = annotationManager.makeWeakPtr(), + .imageManager = imageManager, + .glyphManager = glyphManager, + .prefetchZoomDelta = 0, + .threadPool = {Scheduler::GetBackground(), uniqueID}, + .dynamicTextureAtlas = dynamicTextureAtlas}, style{fileSource, 1, tileParameters.threadPool} {} }; diff --git a/test/tile/raster_dem_tile.test.cpp b/test/tile/raster_dem_tile.test.cpp index ea7eeec94..2519eac70 100644 --- a/test/tile/raster_dem_tile.test.cpp +++ b/test/tile/raster_dem_tile.test.cpp @@ -31,17 +31,17 @@ public: style::Style style; RasterDEMTileTest() - : tileParameters{1.0, - MapDebugOptions(), - transformState, - fileSource, - MapMode::Continuous, - annotationManager.makeWeakPtr(), - imageManager, - glyphManager, - 0, - {Scheduler::GetBackground(), uniqueID}, - dynamicTextureAtlas}, + : tileParameters{.pixelRatio = 1.0, + .debugOptions = MapDebugOptions(), + .transformState = transformState, + .fileSource = fileSource, + .mode = MapMode::Continuous, + .annotationManager = annotationManager.makeWeakPtr(), + .imageManager = imageManager, + .glyphManager = glyphManager, + .prefetchZoomDelta = 0, + .threadPool = {Scheduler::GetBackground(), uniqueID}, + .dynamicTextureAtlas = dynamicTextureAtlas}, style{fileSource, 1, tileParameters.threadPool} {} }; diff --git a/test/tile/raster_tile.test.cpp b/test/tile/raster_tile.test.cpp index a305c3bd2..f716fb645 100644 --- a/test/tile/raster_tile.test.cpp +++ b/test/tile/raster_tile.test.cpp @@ -31,17 +31,17 @@ public: style::Style style; RasterTileTest() - : tileParameters{1.0, - MapDebugOptions(), - transformState, - fileSource, - MapMode::Continuous, - annotationManager.makeWeakPtr(), - imageManager, - glyphManager, - 0, - {Scheduler::GetBackground(), uniqueID}, - dynamicTextureAtlas}, + : tileParameters{.pixelRatio = 1.0, + .debugOptions = MapDebugOptions(), + .transformState = transformState, + .fileSource = fileSource, + .mode = MapMode::Continuous, + .annotationManager = annotationManager.makeWeakPtr(), + .imageManager = imageManager, + .glyphManager = glyphManager, + .prefetchZoomDelta = 0, + .threadPool = {Scheduler::GetBackground(), uniqueID}, + .dynamicTextureAtlas = dynamicTextureAtlas}, style{fileSource, 1, tileParameters.threadPool} {} }; diff --git a/test/tile/tile_lod.test.cpp b/test/tile/tile_lod.test.cpp new file mode 100644 index 000000000..fc3e32d84 --- /dev/null +++ b/test/tile/tile_lod.test.cpp @@ -0,0 +1,156 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace mbgl; +using namespace mbgl::util; +using namespace ::testing; + +class TileLODTest { +public: + util::RunLoop loop; + HeadlessFrontend frontend; + Map map; + + uint32_t tileCount = 0; + +public: + TileLODTest(const CameraOptions& cameraOptions = CameraOptions()) + : frontend(1.0f), + map(frontend, + MapObserver::nullObserver(), + MapOptions().withMapMode(MapMode::Static).withSize(frontend.getSize()), + ResourceOptions().withCachePath(":memory:").withAssetPath("test/fixtures/api/assets")) { + map.jumpTo(cameraOptions); + + map.setDebug(MapDebugOptions::TileBorders); + map.getStyle().loadJSON(util::read_file("test/fixtures/api/empty.json")); + + style::CustomGeometrySource::Options options; + options.fetchTileFunction = [&](const mbgl::CanonicalTileID& tileID) { + auto source = static_cast(map.getStyle().getSource("custom")); + if (!source) { + return; + } + + const LatLngBounds bounds(tileID); + const LatLng& center = bounds.center(); + + mapbox::geojson::feature feature; + feature.geometry = mapbox::geometry::geometry(Point{center.longitude(), center.latitude()}); + + FeatureCollection features; + features.emplace_back(feature); + + source->setTileData(tileID, features); + + ++tileCount; + }; + + map.getStyle().addSource(std::make_unique("custom", options)); + + auto layer = std::make_unique("circles", "custom"); + layer->setCircleColor(Color{0.0, 0.0, 1.0, 0.0}); + map.getStyle().addLayer(std::move(layer)); + } + + void checkImage(std::string image, uint32_t expectedTileCount, double imageThreshold = 0.0002) { + checkImages(std::vector{image}, expectedTileCount, imageThreshold); + } + + void checkImages(std::vector images, uint32_t expectedTileCount, double imageThreshold) { + const auto result = frontend.render(map); + + // check this after frontend.render() + EXPECT_EQ(tileCount, expectedTileCount); + + std::transform(images.begin(), images.end(), images.begin(), [](const auto& img) { + return "test/fixtures/tile_lod/" + img; + }); + + test::checkImages(images, result.image, imageThreshold, 0.01); + } +}; + +TEST(TileLOD, disabled) { + TileLODTest test(CameraOptions().withZoom(10.0)); + + test.map.setTileLodPitchThreshold(std::numbers::pi); + + test.checkImage("disabled", 4); +} + +TEST(TileLOD, zoomShift) { + TileLODTest test(CameraOptions().withZoom(12.0)); + + test.map.setTileLodZoomShift(4.0); + test.frontend.render(test.map); + EXPECT_EQ(test.tileCount, 64); + + test.tileCount = 0; + + test.map.setTileLodZoomShift(-4.0); + test.frontend.render(test.map); + EXPECT_EQ(test.tileCount, 4); +} + +TEST(TileLOD, pitchThreshold) { + TileLODTest test(CameraOptions().withZoom(10.0)); + + constexpr double pitch = 45.0; + test.map.setTileLodZoomShift(4.0); + test.map.setTileLodMinRadius(1.0); + test.map.setTileLodScale(1.0); + test.map.setTileLodPitchThreshold(pitch / 180.0 * std::numbers::pi); + + test.map.jumpTo(CameraOptions().withPitch(pitch - 1.0)); + test.frontend.render(test.map); + EXPECT_EQ(test.tileCount, 140); + test.tileCount = 0; + + test.map.jumpTo(CameraOptions().withPitch(pitch + 1.0)); + test.checkImages( + { + "pitchThreshold-line", + "pitchThreshold-polyline", + }, + 22, + 0.0015); +} + +TEST(TileLOD, scale) { + TileLODTest test(CameraOptions().withZoom(0.0)); + + test.map.setTileLodMinRadius(1.0); + test.map.setTileLodScale(0.4); + test.map.setTileLodPitchThreshold(-1.0); + + test.map.setTileLodZoomShift(5.0); + + test.checkImage("scale", 112); +} + +TEST(TileLOD, radius) { + TileLODTest test(CameraOptions().withZoom(0.0)); + + test.map.setTileLodMinRadius(5.0); + test.map.setTileLodScale(1.0); + test.map.setTileLodPitchThreshold(-1.0); + + test.map.setTileLodZoomShift(5.0); + + test.checkImage("radius", 172); +} diff --git a/test/util/tile_cover.test.cpp b/test/util/tile_cover.test.cpp index 535029d1d..a84119d2d 100644 --- a/test/util/tile_cover.test.cpp +++ b/test/util/tile_cover.test.cpp @@ -44,7 +44,7 @@ TEST(TileCover, Pitch) { .withPitch(40.0)); EXPECT_EQ((std::vector{{2, 1, 1}, {2, 2, 1}, {2, 1, 2}, {2, 2, 2}}), - util::tileCover(transform.getState(), 2)); + util::tileCover({transform.getState()}, 2)); } TEST(TileCover, PitchIssue15442) { @@ -62,7 +62,7 @@ TEST(TileCover, PitchIssue15442) { EXPECT_EQ((std::vector{ {2, 3, 1}, {2, 2, 1}, {2, 3, 0}, {2, 2, 0}, {2, 1, {2, 0, 0}}, {2, 1, {2, 1, 0}}}), - util::tileCover(transform.getState(), 2)); + util::tileCover({transform.getState()}, 2)); } TEST(TileCover, PitchOverAllowedByContentInsets) { @@ -80,7 +80,7 @@ TEST(TileCover, PitchOverAllowedByContentInsets) { EXPECT_EQ( (std::vector{{3, 4, 3}, {3, 3, 3}, {3, 4, 4}, {3, 3, 4}, {3, 4, 2}, {3, 5, 3}, {3, 5, 2}}), - util::tileCover(transform.getState(), 3)); + util::tileCover({transform.getState()}, 3)); } TEST(TileCover, PitchWithLargerResultSet) { @@ -96,7 +96,7 @@ TEST(TileCover, PitchWithLargerResultSet) { .withBearing(-142.2630000003529176) .withPitch(60.0)); - auto cover = util::tileCover(transform.getState(), 5); + auto cover = util::tileCover({transform.getState()}, 5); // Returned vector has above 100 elements, we check first 16 as there is a // plan to return lower LOD for distant tiles. EXPECT_EQ((std::vector{{5, 15, 16}, @@ -148,7 +148,7 @@ TEST(TileCover, CoordinatesAreUnwrapped) { EXPECT_EQ( (std::vector{{1, 0, {1, 1, 0}}, {1, 1, {1, 0, 0}}, {1, 0, {1, 1, 1}}, {1, 1, {1, 0, 1}}}), - util::tileCover(transform.getState(), 1)); + util::tileCover({transform.getState()}, 1)); } TEST(TileCover, DifferentOverscaledZ) { @@ -165,7 +165,7 @@ TEST(TileCover, DifferentOverscaledZ) { EXPECT_EQ( (std::vector{{3, 0, {2, 1, 1}}, {3, 0, {2, 2, 1}}, {3, 0, {2, 1, 2}}, {3, 0, {2, 2, 2}}}), - util::tileCover(transform.getState(), 2, 3)); + util::tileCover({transform.getState()}, 2, 3)); } TEST(TileCover, DifferentOverscaledZWithPitch) { @@ -186,7 +186,7 @@ TEST(TileCover, DifferentOverscaledZWithPitch) { {5, 0, {3, 3, 1}}, {5, 0, {3, 4, 1}}, {5, 0, {3, 2, 1}}}), - util::tileCover(transform.getState(), 3, 5)); + util::tileCover({transform.getState()}, 3, 5)); } TEST(TileCover, DifferentOverscaledZWrapped) { @@ -201,7 +201,7 @@ TEST(TileCover, DifferentOverscaledZWrapped) { EXPECT_EQ( (std::vector{{2, 0, {1, 1, 0}}, {2, 1, {1, 0, 0}}, {2, 0, {1, 1, 1}}, {2, 1, {1, 0, 1}}}), - util::tileCover(transform.getState(), 1, 2)); + util::tileCover({transform.getState()}, 1, 2)); } TEST(TileCover, FlippedY) { @@ -216,7 +216,7 @@ TEST(TileCover, FlippedY) { .withZoom(1.0)); EXPECT_EQ((std::vector{{1, 0, 0}, {1, 1, 0}, {1, 0, 1}, {1, 1, 1}}), - util::tileCover(transform.getState(), 1)); + util::tileCover({transform.getState()}, 1)); } TEST(TileCover, FlippedYPitch) { @@ -233,7 +233,7 @@ TEST(TileCover, FlippedYPitch) { .withPitch(40.0)); EXPECT_EQ((std::vector{{2, 1, 1}, {2, 2, 1}, {2, 1, 2}, {2, 2, 2}}), - util::tileCover(transform.getState(), 2)); + util::tileCover({transform.getState()}, 2)); } TEST(TileCover, FlippedYHelsinki) { @@ -248,7 +248,7 @@ TEST(TileCover, FlippedYHelsinki) { .withZoom(11.447425)); EXPECT_EQ((std::vector{{11, 1165, 592}, {11, 1166, 592}, {11, 1165, 593}, {11, 1166, 593}}), - util::tileCover(transform.getState(), 11)); + util::tileCover({transform.getState()}, 11)); } TEST(TileCoverStream, Arctic) {