core, android: add support for camera roll (#3011)

This commit is contained in:
Nathan Olson
2026-01-20 00:57:48 -08:00
committed by GitHub
parent a4cf4d3ffa
commit 10dbd5d68d
57 changed files with 1275 additions and 207 deletions

View File

@@ -39,9 +39,11 @@ int main(int argc, char* argv[]) {
args::ValueFlag<double> zoomValue(centerGroup, "number", "Zoom level", {'z', "zoom"});
args::ValueFlag<double> lonValue(centerGroup, "degrees", "Longitude", {'x', "lon"});
args::ValueFlag<double> latValue(centerGroup, "degrees", "Latitude", {'y', "lat"});
args::ValueFlag<double> altValue(argumentParser, "degrees", "Altitude", {'A', "alt"});
args::ValueFlag<double> fovValue(argumentParser, "degrees", "FOV", {'f', "fov"});
args::ValueFlag<double> bearingValue(argumentParser, "degrees", "Bearing", {'b', "bearing"});
args::ValueFlag<double> pitchValue(argumentParser, "degrees", "Pitch", {'p', "pitch"});
args::ValueFlag<double> rollValue(argumentParser, "degrees", "Roll", {'R', "roll"});
args::ValueFlag<uint32_t> widthValue(argumentParser, "pixels", "Image width", {'w', "width"});
args::ValueFlag<uint32_t> heightValue(argumentParser, "pixels", "Image height", {'h', "height"});
@@ -65,9 +67,12 @@ int main(int argc, char* argv[]) {
const double lat = latValue ? args::get(latValue) : 0;
const double lon = lonValue ? args::get(lonValue) : 0;
const double alt = altValue ? args::get(altValue) : 0;
const double zoom = zoomValue ? args::get(zoomValue) : 0;
const double fov = fovValue ? args::get(fovValue) : mbgl::util::rad2deg(mbgl::util::DEFAULT_FOV);
const double bearing = bearingValue ? args::get(bearingValue) : 0;
const double pitch = pitchValue ? args::get(pitchValue) : 0;
const double roll = rollValue ? args::get(rollValue) : 0;
const double pixelRatio = pixelRatioValue ? args::get(pixelRatioValue) : 1;
const uint32_t width = widthValue ? args::get(widthValue) : 512;
@@ -120,7 +125,14 @@ int main(int argc, char* argv[]) {
LatLngBounds boundingBox = LatLngBounds::hull(LatLng(bounds[0], bounds[1]), LatLng(bounds[2], bounds[3]));
map.jumpTo(map.cameraForLatLngBounds(boundingBox, EdgeInsets(), bearing, pitch));
} else {
map.jumpTo(CameraOptions().withCenter(LatLng{lat, lon}).withZoom(zoom).withBearing(bearing).withPitch(pitch));
map.jumpTo(CameraOptions()
.withCenter(LatLng{lat, lon})
.withCenterAltitude(alt)
.withZoom(zoom)
.withBearing(bearing)
.withPitch(pitch)
.withRoll(roll)
.withFov(fov));
}
if (debug) {

View File

@@ -21,6 +21,10 @@ struct CameraOptions {
center = o;
return *this;
}
CameraOptions& withCenterAltitude(const std::optional<double>& o) {
centerAltitude = o;
return *this;
}
CameraOptions& withPadding(const std::optional<EdgeInsets>& p) {
padding = p;
return *this;
@@ -41,10 +45,21 @@ struct CameraOptions {
pitch = o;
return *this;
}
CameraOptions& withRoll(const std::optional<double>& o) {
roll = o;
return *this;
}
CameraOptions& withFov(const std::optional<double>& o) {
fov = o;
return *this;
}
/** Coordinate at the center of the map. */
std::optional<LatLng> center;
/** Altitude of the center of the map, in meters above sea level. */
std::optional<double> centerAltitude;
/** Padding around the interior of the view that affects the frame of
reference for `center`. */
std::optional<EdgeInsets> padding;
@@ -63,11 +78,18 @@ struct CameraOptions {
/** Pitch toward the horizon measured in degrees , with 0 deg resulting in a
two-dimensional map. */
std::optional<double> pitch;
/** Camera roll, measured in degrees. */
std::optional<double> roll;
/** Camera vertical field of view, measured in degrees. */
std::optional<double> fov;
};
constexpr bool operator==(const CameraOptions& a, const CameraOptions& b) {
return a.center == b.center && a.padding == b.padding && a.anchor == b.anchor && a.zoom == b.zoom &&
a.bearing == b.bearing && a.pitch == b.pitch;
a.bearing == b.bearing && a.pitch == b.pitch && a.roll == b.roll && a.fov == b.fov &&
a.centerAltitude == b.centerAltitude;
}
constexpr bool operator!=(const CameraOptions& a, const CameraOptions& b) {
@@ -134,7 +156,6 @@ struct FreeCameraOptions {
0, 0]
Orientation can be set freely but certain constraints still apply
- Orientation must be representable with only pitch and bearing.
- Pitch has an upper limit */
std::optional<vec4> orientation = std::nullopt;
@@ -151,9 +172,9 @@ struct FreeCameraOptions {
bearing can't be deduced from the viewing direction */
void lookAtPoint(const LatLng& location, const std::optional<vec3>& upVector = std::nullopt) noexcept;
/** Helper function for setting the orientation of the camera as a pitch and
a bearing. Both values are in degrees */
void setPitchBearing(double pitch, double bearing) noexcept;
/** Helper function for setting the orientation of the camera as a roll, pitch, and
a bearing. All values are in degrees */
void setRollPitchBearing(double roll, double pitch, double bearing) noexcept;
};
} // namespace mbgl

View File

@@ -32,7 +32,13 @@ constexpr double LATITUDE_MAX = 85.051128779806604;
constexpr double LONGITUDE_MAX = 180;
constexpr double DEGREES_MAX = 360;
constexpr double PITCH_MIN = 0.0;
constexpr double PITCH_MAX = M_PI / 3;
constexpr double DEFAULT_PITCH_MAX = M_PI / 3;
constexpr double PITCH_MAX = M_PI;
// This fov value is somewhat arbitrary. The altitude of the camera used
// to be defined as 1.5 screen heights above the ground, which was an
// arbitrary choice. This is the fov equivalent to that value calculated
// with: `fov = 2 * arctan((height / 2) / (height * 1.5))`
constexpr double DEFAULT_FOV = 0.6435011087932844;
constexpr double MIN_ZOOM = 0.0;
constexpr double MAX_ZOOM = 25.5;
constexpr float MIN_ZOOM_F = MIN_ZOOM;

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

View File

@@ -0,0 +1,37 @@
{
"version": 8,
"metadata": {
"test": {
"width": 64,
"height": 64
}
},
"bearing": 45,
"pitch": 45,
"roll": 45,
"sources": {
"geojson": {
"type": "geojson",
"data": {
"type": "Point",
"coordinates": [
0,
0
]
}
}
},
"sprite": "local://sprites/sprite",
"layers": [
{
"id": "symbol",
"type": "symbol",
"source": "geojson",
"layout": {
"symbol-placement": "point",
"icon-rotation-alignment": "map",
"icon-image": "oneway"
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 B

View File

@@ -0,0 +1,36 @@
{
"version": 8,
"metadata": {
"test": {
"width": 64,
"height": 64
}
},
"bearing": 45,
"roll": 45,
"sources": {
"geojson": {
"type": "geojson",
"data": {
"type": "Point",
"coordinates": [
0,
0
]
}
}
},
"sprite": "local://sprites/sprite",
"layers": [
{
"id": "symbol",
"type": "symbol",
"source": "geojson",
"layout": {
"symbol-placement": "point",
"icon-rotation-alignment": "auto",
"icon-image": "oneway"
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

View File

@@ -0,0 +1,38 @@
{
"version": 8,
"metadata": {
"test": {
"width": 64,
"height": 64
}
},
"bearing": 45,
"pitch": 45,
"roll": 45,
"sources": {
"geojson": {
"type": "geojson",
"data": {
"type": "Point",
"coordinates": [
0,
0
]
}
}
},
"sprite": "local://sprites/sprite",
"layers": [
{
"id": "symbol",
"type": "symbol",
"source": "geojson",
"layout": {
"symbol-placement": "point",
"icon-rotation-alignment": "viewport",
"icon-pitch-alignment": "map",
"icon-image": "oneway"
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

View File

@@ -0,0 +1,38 @@
{
"version": 8,
"metadata": {
"test": {
"width": 64,
"height": 64
}
},
"bearing": 45,
"pitch": 45,
"roll": 45,
"sources": {
"geojson": {
"type": "geojson",
"data": {
"type": "Point",
"coordinates": [
0,
0
]
}
}
},
"sprite": "local://sprites/sprite",
"layers": [
{
"id": "symbol",
"type": "symbol",
"source": "geojson",
"layout": {
"symbol-placement": "point",
"icon-rotation-alignment": "map",
"icon-pitch-alignment": "viewport",
"icon-image": "oneway"
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,58 @@
{
"version": 8,
"metadata": {
"test": {
"height": 256,
"operations": [
[ "setBearing", 90 ],
[ "setPitch", 40 ],
[ "setRoll", 90 ],
[ "wait" ]
]
}
},
"center": [
13.418056,
52.499167
],
"zoom": 14,
"sources": {
"maplibre": {
"type": "vector",
"maxzoom": 14,
"tiles": [
"local://tiles/{z}-{x}-{y}.mvt"
]
}
},
"glyphs": "local://glyphs/{fontstack}/{range}.pbf",
"layers": [
{
"id": "background",
"type": "background",
"paint": {
"background-color": "white"
}
},
{
"id": "line-placement-true",
"type": "symbol",
"source": "maplibre",
"source-layer": "road",
"layout": {
"symbol-placement": "line",
"text-allow-overlap": true,
"text-ignore-placement": true,
"text-field": "{class} {class}",
"text-font": [
"Open Sans Semibold",
"Arial Unicode MS Bold"
],
"text-keep-upright": true
},
"paint": {
"text-opacity": 1
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,56 @@
{
"version": 8,
"metadata": {
"test": {
"height": 256,
"operations": [
[ "setRoll", 180 ],
[ "wait" ]
]
}
},
"center": [
13.418056,
52.499167
],
"zoom": 14,
"sources": {
"maplibre": {
"type": "vector",
"maxzoom": 14,
"tiles": [
"local://tiles/{z}-{x}-{y}.mvt"
]
}
},
"glyphs": "local://glyphs/{fontstack}/{range}.pbf",
"layers": [
{
"id": "background",
"type": "background",
"paint": {
"background-color": "white"
}
},
{
"id": "line-placement-true",
"type": "symbol",
"source": "maplibre",
"source-layer": "road",
"layout": {
"symbol-placement": "line",
"text-allow-overlap": true,
"text-ignore-placement": true,
"text-field": "{class} {class}",
"text-font": [
"Open Sans Semibold",
"Arial Unicode MS Bold"
],
"text-keep-upright": true
},
"paint": {
"text-opacity": 1
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -0,0 +1,65 @@
{
"version": 8,
"metadata": {
"test": {
"collisionDebug": true,
"height": 512
}
},
"center": [
13.418056,
52.499167
],
"zoom": 16,
"pitch": 30,
"roll": 180,
"sources": {
"maplibre": {
"type": "vector",
"maxzoom": 14,
"tiles": [
"local://tiles/{z}-{x}-{y}.mvt"
]
}
},
"glyphs": "local://glyphs/{fontstack}/{range}.pbf",
"layers": [
{
"id": "background",
"type": "background",
"paint": {
"background-color": "white"
}
},
{
"id": "road",
"type": "line",
"source": "maplibre",
"source-layer": "road",
"paint": {
"line-color": "#888",
"line-width": 1
}
},
{
"id": "text",
"type": "symbol",
"source": "maplibre",
"source-layer": "road_label",
"layout": {
"symbol-placement": "line",
"symbol-spacing": 60,
"text-rotation-alignment": "map",
"text-pitch-alignment": "auto",
"text-field": "{class}",
"text-font": [
"Open Sans Semibold",
"Arial Unicode MS Bold"
]
},
"paint": {
"text-opacity": 1
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

View File

@@ -0,0 +1,47 @@
{
"version": 8,
"metadata": {
"test": {
"width": 128,
"height": 128
}
},
"roll": 45,
"sources": {
"geojson": {
"type": "geojson",
"data": {
"type": "LineString",
"coordinates": [
[
-40,
0
],
[
40,
0
]
]
}
}
},
"glyphs": "local://glyphs/{fontstack}/{range}.pbf",
"layers": [
{
"id": "symbol",
"type": "symbol",
"source": "geojson",
"layout": {
"symbol-placement": "line",
"symbol-spacing": 20,
"text-allow-overlap": true,
"text-rotation-alignment": "auto",
"text-field": "-->",
"text-font": [
"Open Sans Semibold",
"Arial Unicode MS Bold"
]
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

View File

@@ -0,0 +1,39 @@
{
"version": 8,
"metadata": {
"test": {
"width": 128,
"height": 128
}
},
"roll": 45,
"sources": {
"geojson": {
"type": "geojson",
"data": {
"type": "Point",
"coordinates": [
0,
0
]
}
}
},
"glyphs": "local://glyphs/{fontstack}/{range}.pbf",
"layers": [
{
"id": "symbol",
"type": "symbol",
"source": "geojson",
"layout": {
"symbol-placement": "point",
"text-rotation-alignment": "auto",
"text-field": "-->",
"text-font": [
"Open Sans Semibold",
"Arial Unicode MS Bold"
]
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

View File

@@ -0,0 +1,47 @@
{
"version": 8,
"metadata": {
"test": {
"width": 128,
"height": 128
}
},
"roll": 45,
"sources": {
"geojson": {
"type": "geojson",
"data": {
"type": "LineString",
"coordinates": [
[
-40,
0
],
[
40,
0
]
]
}
}
},
"glyphs": "local://glyphs/{fontstack}/{range}.pbf",
"layers": [
{
"id": "symbol",
"type": "symbol",
"source": "geojson",
"layout": {
"symbol-placement": "line",
"symbol-spacing": 20,
"text-allow-overlap": true,
"text-rotation-alignment": "map",
"text-field": "-->",
"text-font": [
"Open Sans Semibold",
"Arial Unicode MS Bold"
]
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

View File

@@ -0,0 +1,39 @@
{
"version": 8,
"metadata": {
"test": {
"width": 128,
"height": 128
}
},
"roll": 45,
"sources": {
"geojson": {
"type": "geojson",
"data": {
"type": "Point",
"coordinates": [
0,
0
]
}
}
},
"glyphs": "local://glyphs/{fontstack}/{range}.pbf",
"layers": [
{
"id": "symbol",
"type": "symbol",
"source": "geojson",
"layout": {
"symbol-placement": "point",
"text-rotation-alignment": "map",
"text-field": "-->",
"text-font": [
"Open Sans Semibold",
"Arial Unicode MS Bold"
]
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

View File

@@ -0,0 +1,47 @@
{
"version": 8,
"metadata": {
"test": {
"width": 128,
"height": 128
}
},
"roll": 45,
"sources": {
"geojson": {
"type": "geojson",
"data": {
"type": "LineString",
"coordinates": [
[
-40,
0
],
[
40,
0
]
]
}
}
},
"glyphs": "local://glyphs/{fontstack}/{range}.pbf",
"layers": [
{
"id": "symbol",
"type": "symbol",
"source": "geojson",
"layout": {
"symbol-placement": "line",
"symbol-spacing": 20,
"text-allow-overlap": true,
"text-rotation-alignment": "viewport",
"text-field": "-->",
"text-font": [
"Open Sans Semibold",
"Arial Unicode MS Bold"
]
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

View File

@@ -0,0 +1,39 @@
{
"version": 8,
"metadata": {
"test": {
"width": 128,
"height": 128
}
},
"roll": 45,
"sources": {
"geojson": {
"type": "geojson",
"data": {
"type": "Point",
"coordinates": [
0,
0
]
}
}
},
"glyphs": "local://glyphs/{fontstack}/{range}.pbf",
"layers": [
{
"id": "symbol",
"type": "symbol",
"source": "geojson",
"layout": {
"symbol-placement": "point",
"text-rotation-alignment": "viewport",
"text-field": "-->",
"text-font": [
"Open Sans Semibold",
"Arial Unicode MS Bold"
]
}
}
]
}

View File

@@ -1,5 +1,7 @@
#include "camera_position.hpp"
#include "../geometry/lat_lng.hpp"
#include <mbgl/util/constants.hpp>
#include <mbgl/math/angles.hpp>
namespace mbgl {
namespace android {
@@ -8,12 +10,19 @@ jni::Local<jni::Object<CameraPosition>> CameraPosition::New(jni::JNIEnv& env,
mbgl::CameraOptions options,
float pixelRatio) {
static auto& javaClass = jni::Class<CameraPosition>::Singleton(env);
static auto constructor =
javaClass.GetConstructor<jni::Object<LatLng>, double, double, double, jni::Array<jni::jdouble>>(env);
static auto constructor = javaClass.GetConstructor<jni::Object<LatLng>,
double,
double,
double,
double,
double,
double,
jni::Array<jni::jdouble>>(env);
// wrap LatLng values coming from core
auto center = options.center.value();
center.wrap();
double centerAltitude = options.centerAltitude.value_or(0);
// convert bearing, measured in radians counterclockwise from true north.
// Wrapped to [−π rad, π rad). Android binding from 0 to 360 degrees
@@ -27,6 +36,8 @@ jni::Local<jni::Object<CameraPosition>> CameraPosition::New(jni::JNIEnv& env,
// convert tilt, core ranges from [0 rad, 1,0472 rad], android ranges from 0 to 60
double tilt_degrees = options.pitch.value_or(0);
double roll_degrees = options.roll.value_or(0);
double fov_degrees = options.fov.value_or(util::rad2deg(util::DEFAULT_FOV));
std::vector<jdouble> paddingVect;
auto insets = options.padding.value_or(EdgeInsets{0, 0, 0, 0});
@@ -37,8 +48,16 @@ jni::Local<jni::Object<CameraPosition>> CameraPosition::New(jni::JNIEnv& env,
paddingVect.push_back(insets.bottom() * pixelRatio);
padding.SetRegion<std::vector<jni::jdouble>>(env, 0, paddingVect);
return javaClass.New(
env, constructor, LatLng::New(env, center), options.zoom.value_or(0), tilt_degrees, bearing_degrees, padding);
return javaClass.New(env,
constructor,
LatLng::New(env, center),
centerAltitude,
options.zoom.value_or(0),
tilt_degrees,
bearing_degrees,
roll_degrees,
fov_degrees,
padding);
}
mbgl::CameraOptions CameraPosition::getCameraOptions(jni::JNIEnv& env,
@@ -50,11 +69,15 @@ mbgl::CameraOptions CameraPosition::getCameraOptions(jni::JNIEnv& env,
static auto tilt = javaClass.GetField<jni::jdouble>(env, "tilt");
static auto zoom = javaClass.GetField<jni::jdouble>(env, "zoom");
static auto paddingField = javaClass.GetField<jni::Array<jni::jdouble>>(env, "padding");
static auto centerAltitude = javaClass.GetField<jni::jdouble>(env, "centerAltitude");
static auto roll = javaClass.GetField<jni::jdouble>(env, "roll");
static auto fov = javaClass.GetField<jni::jdouble>(env, "fov");
auto padding = position.Get(env, paddingField);
auto center = LatLng::getLatLng(env, position.Get(env, target));
return mbgl::CameraOptions{center,
position.Get(env, centerAltitude),
padding && padding.Length(env) == 4 ? EdgeInsets{padding.Get(env, 1) * pixelRatio,
padding.Get(env, 0) * pixelRatio,
padding.Get(env, 3) * pixelRatio,
@@ -63,7 +86,9 @@ mbgl::CameraOptions CameraPosition::getCameraOptions(jni::JNIEnv& env,
{},
position.Get(env, zoom),
position.Get(env, bearing),
position.Get(env, tilt)};
position.Get(env, tilt),
position.Get(env, roll),
position.Get(env, fov)};
}
void CameraPosition::registerNative(jni::JNIEnv& env) {

View File

@@ -24,16 +24,21 @@ class CameraPosition
* Constructs a CameraPosition.
*
* @param target The target location to align with the center of the screen.
* @param centerAltitude The altitude of the target location to align with the center of the
* screen, in meters above sea level.
* @param zoom Zoom level at target. See zoom(float) for details of restrictions.
* @param tilt The camera angle, in degrees, from the nadir (directly down). See tilt(float)
* for details of restrictions.
* @param bearing Direction that the camera is pointing in, in degrees clockwise from north.
* This value will be normalized to be within 0 degrees inclusive and 360 degrees
* exclusive.
* @param roll Roll of the camera, in degrees clockwise.
* @param fov Vertical field of view of the camera, in degrees.
* @param padding Padding in pixels. Specified in left, top, right, bottom order.
* @throws NullPointerException if target is null
* @throws IllegalArgumentException if tilt is outside the range of 0 to 90 degrees inclusive.
*/ @Keep internal constructor(
*/
@Keep internal constructor(
/**
* The location that the camera is pointing at.
@@ -42,6 +47,13 @@ class CameraPosition
@JvmField
val target: LatLng?,
/**
* The altitude of the location that the camera is pointing at, in meters above sea level.
*/
@field:Keep
@JvmField
val centerAltitude: Double,
/**
* Zoom level near the center of the screen. See [Builder.zoom] for the definition of the camera's
* zoom level.
@@ -65,6 +77,20 @@ class CameraPosition
@JvmField
val bearing: Double,
/**
* Roll of the camera, in degrees clockwise.
*/
@field:Keep
@JvmField
val roll: Double,
/**
* Vertical field of view of the camera, in degrees.
*/
@field:Keep
@JvmField
val fov: Double,
/**
* Padding in pixels. Specified in left, top, right, bottom order.
* See [Builder.padding] for the definition of the camera's padding.
@@ -75,6 +101,22 @@ class CameraPosition
) : Parcelable {
/**
* Constructs a CameraPosition.
*
* @param target The target location to align with the center of the screen.
* @param zoom Zoom level at target. See zoom(float) for details of restrictions.
* @param tilt The camera angle, in degrees, from the nadir (directly down). See tilt(float)
* for details of restrictions.
* @param bearing Direction that the camera is pointing in, in degrees clockwise from north.
* This value will be normalized to be within 0 degrees inclusive and 360 degrees
* exclusive.
* @param padding Padding in pixels. Specified in left, top, right, bottom order.
* @throws NullPointerException if target is null
* @throws IllegalArgumentException if tilt is outside the range of 0 to 90 degrees inclusive.
*/
@Keep internal constructor(target: LatLng?, zoom: Double, tilt: Double, bearing: Double, padding: DoubleArray?) : this(target, 0.0, zoom, tilt, bearing, 0.0, MapLibreConstants.DEFAULT_FOV, padding)
/**
* Constructs a CameraPosition.
*
@@ -112,7 +154,10 @@ class CameraPosition
override fun writeToParcel(out: Parcel, flags: Int) {
out.writeDouble(bearing)
out.writeParcelable(target, flags)
out.writeDouble(centerAltitude)
out.writeDouble(tilt)
out.writeDouble(roll)
out.writeDouble(fov)
out.writeDouble(zoom)
if (padding != null) {
val length = padding.size
@@ -131,7 +176,7 @@ class CameraPosition
* @return A String with CameraPosition information.
*/
override fun toString(): String {
return ("Target: " + target + ", Zoom:" + zoom + ", Bearing:" + bearing + ", Tilt:" + tilt + ", Padding:" + Arrays.toString(padding))
return ("Target: " + target + ", Center Altitude:" + centerAltitude + ", Zoom:" + zoom + ", Bearing:" + bearing + ", Tilt:" + tilt + ", Roll:" + roll + ", FOV:" + fov + ", Padding:" + Arrays.toString(padding))
}
/**
@@ -152,10 +197,16 @@ class CameraPosition
val cameraPosition = other as CameraPosition
if (target != null && target != cameraPosition.target) {
return false
} else if (centerAltitude != cameraPosition.centerAltitude) {
return false
} else if (zoom != cameraPosition.zoom) {
return false
} else if (tilt != cameraPosition.tilt) {
return false
} else if (roll != cameraPosition.roll) {
return false
} else if (fov != cameraPosition.fov) {
return false
} else if (bearing != cameraPosition.bearing) {
return false
} else if (!Arrays.equals(padding, cameraPosition.padding)) {
@@ -178,8 +229,14 @@ class CameraPosition
temp = java.lang.Double.doubleToLongBits(bearing)
result = (temp xor (temp ushr 32)).toInt()
result = 31 * result + (target?.hashCode() ?: 0)
temp = java.lang.Double.doubleToLongBits(centerAltitude)
result = 31 * result + (temp xor (temp ushr 32)).toInt()
temp = java.lang.Double.doubleToLongBits(tilt)
result = 31 * result + (temp xor (temp ushr 32)).toInt()
temp = java.lang.Double.doubleToLongBits(roll)
result = 31 * result + (temp xor (temp ushr 32)).toInt()
temp = java.lang.Double.doubleToLongBits(fov)
result = 31 * result + (temp xor (temp ushr 32)).toInt()
temp = java.lang.Double.doubleToLongBits(zoom)
result = 31 * result + (temp xor (temp ushr 32)).toInt()
result = 31 * result + Arrays.hashCode(padding)
@@ -192,7 +249,10 @@ class CameraPosition
class Builder {
private var bearing = -1.0
private var target: LatLng? = null
private var centerAltitude = -1.0
private var tilt = -1.0
private var roll = -1.0
private var fov = -1.0
private var zoom = -1.0
private var padding: DoubleArray? = null
@@ -210,7 +270,10 @@ class CameraPosition
if (previous != null) {
bearing = previous.bearing
target = previous.target
centerAltitude = previous.centerAltitude
tilt = previous.tilt
roll = previous.roll
fov = previous.fov
zoom = previous.zoom
padding = previous.padding
}
@@ -227,7 +290,10 @@ class CameraPosition
val lat = typedArray.getFloat(R.styleable.maplibre_MapView_maplibre_cameraTargetLat, 0.0f).toDouble()
val lng = typedArray.getFloat(R.styleable.maplibre_MapView_maplibre_cameraTargetLng, 0.0f).toDouble()
target = LatLng(lat, lng)
centerAltitude = typedArray.getFloat(R.styleable.maplibre_MapView_maplibre_cameraCenterAltitude, 0.0f).toDouble()
tilt = typedArray.getFloat(R.styleable.maplibre_MapView_maplibre_cameraTilt, 0.0f).toDouble()
roll = typedArray.getFloat(R.styleable.maplibre_MapView_maplibre_cameraRoll, 0.0f).toDouble()
fov = typedArray.getFloat(R.styleable.maplibre_MapView_maplibre_cameraFov, 36.86989764584402f).toDouble()
zoom = typedArray.getFloat(R.styleable.maplibre_MapView_maplibre_cameraZoom, 0.0f).toDouble()
}
}
@@ -241,7 +307,10 @@ class CameraPosition
if (update != null) {
bearing = update.bearing
target = update.target
centerAltitude = update.centerAltitude
tilt = update.tilt
roll = update.roll
fov = update.fov
zoom = update.zoom
padding = update.padding
}
@@ -287,6 +356,17 @@ class CameraPosition
return this
}
/**
* Set the altitude of the location where the camera is pointing at in meters above sea level
*
* @param centerAltitude altitude of the location where the camera is pointing at
* @return this
*/
fun centerAltitude(centerAltitude: Double): Builder {
this.centerAltitude = centerAltitude
return this
}
/**
* Set the tilt of the camera in degrees
*
@@ -302,6 +382,28 @@ class CameraPosition
return this
}
/**
* Set the roll of the camera in degrees
*
* @param roll Roll value of the camera
* @return this
*/
fun roll(roll: Double): Builder {
this.roll = roll
return this
}
/**
* Set the vertical field of view of the camera in degrees
*
* @param fov Vertical field of view of the camera
* @return this
*/
fun fov(fov: Double): Builder {
this.fov = fov
return this
}
/**
* Set the zoom of the camera
*
@@ -375,7 +477,10 @@ class CameraPosition
} else {
parcel.readParcelable(LatLng::class.java.classLoader)
}
val centerAltitude = parcel.readDouble()
val tilt = parcel.readDouble()
val roll = parcel.readDouble()
val fov = parcel.readDouble()
val zoom = parcel.readDouble()
var padding: DoubleArray? = null
val paddingSize = parcel.readInt()
@@ -385,7 +490,7 @@ class CameraPosition
padding[i] = parcel.readDouble()
}
}
return CameraPosition(target, zoom, tilt, bearing, padding)
return CameraPosition(target, centerAltitude, zoom, tilt, bearing, roll, fov, padding)
}
override fun newArray(size: Int): Array<CameraPosition?> {

View File

@@ -22,7 +22,7 @@ object CameraUpdateFactory {
*/
@JvmStatic
fun newCameraPosition(cameraPosition: CameraPosition): CameraUpdate {
return CameraPositionUpdate(cameraPosition.bearing, cameraPosition.target, cameraPosition.tilt, cameraPosition.zoom, cameraPosition.padding)
return CameraPositionUpdate(cameraPosition.bearing, cameraPosition.target, cameraPosition.centerAltitude, cameraPosition.tilt, cameraPosition.roll, cameraPosition.fov, cameraPosition.zoom, cameraPosition.padding)
}
/**
@@ -34,7 +34,7 @@ object CameraUpdateFactory {
*/
@JvmStatic
fun newLatLng(latLng: LatLng): CameraUpdate {
return CameraPositionUpdate(-1.0, latLng, -1.0, -1.0, null)
return CameraPositionUpdate(-1.0, latLng, -1.0, -1.0, -1.0, -1.0, -1.0, null)
}
/**
@@ -133,7 +133,7 @@ object CameraUpdateFactory {
*/
@JvmStatic
fun newLatLngZoom(latLng: LatLng, zoom: Double): CameraUpdate {
return CameraPositionUpdate(-1.0, latLng, -1.0, zoom, null)
return CameraPositionUpdate(-1.0, latLng, -1.0, -1.0, -1.0, -1.0, zoom, null)
}
/**
@@ -150,7 +150,7 @@ object CameraUpdateFactory {
*/
@JvmStatic
fun newLatLngPadding(latLng: LatLng, left: Double, top: Double, right: Double, bottom: Double): CameraUpdate {
return CameraPositionUpdate(-1.0, latLng, -1.0, -1.0, doubleArrayOf(left, top, right, bottom))
return CameraPositionUpdate(-1.0, latLng, -1.0, -1.0, -1.0, -1.0, -1.0, doubleArrayOf(left, top, right, bottom))
}
/**
@@ -217,7 +217,7 @@ object CameraUpdateFactory {
*/
@JvmStatic
fun bearingTo(bearing: Double): CameraUpdate {
return CameraPositionUpdate(bearing, null, -1.0, -1.0, null)
return CameraPositionUpdate(bearing, null, -1.0, -1.0, -1.0, -1.0, -1.0, null)
}
/**
@@ -228,7 +228,7 @@ object CameraUpdateFactory {
*/
@JvmStatic
fun tiltTo(tilt: Double): CameraUpdate {
return CameraPositionUpdate(-1.0, null, tilt, -1.0, null)
return CameraPositionUpdate(-1.0, null, -1.0, tilt, -1.0, -1.0, -1.0, null)
}
/**
@@ -244,7 +244,7 @@ object CameraUpdateFactory {
*/
@JvmStatic
fun paddingTo(padding: DoubleArray?): CameraUpdate {
return CameraPositionUpdate(-1.0, null, -1.0, -1.0, padding)
return CameraPositionUpdate(-1.0, null, -1.0, -1.0, -1.0, -1.0, -1.0, padding)
}
/**
@@ -265,7 +265,7 @@ object CameraUpdateFactory {
//
// CameraUpdate types
//
class CameraPositionUpdate(val bearing: Double, val target: LatLng?, val tilt: Double, val zoom: Double, val padding: DoubleArray?) : CameraUpdate {
class CameraPositionUpdate(val bearing: Double, val target: LatLng?, val centerAltitude: Double, val tilt: Double, val roll: Double, val fov: Double, val zoom: Double, val padding: DoubleArray?) : CameraUpdate {
override fun getCameraPosition(maplibreMap: MapLibreMap): CameraPosition {
if (target == null) {
@@ -289,9 +289,18 @@ object CameraUpdateFactory {
if (java.lang.Double.compare(that.tilt, tilt) != 0) {
return false
}
if (java.lang.Double.compare(that.roll, roll) != 0) {
return false
}
if (java.lang.Double.compare(that.fov, fov) != 0) {
return false
}
if (java.lang.Double.compare(that.zoom, zoom) != 0) {
return false
}
if (java.lang.Double.compare(that.centerAltitude, centerAltitude) != 0) {
return false
}
return if (if (target != null) target != that.target else that.target != null) {
false
} else {
@@ -305,8 +314,14 @@ object CameraUpdateFactory {
temp = java.lang.Double.doubleToLongBits(bearing)
result = (temp xor (temp ushr 32)).toInt()
result = 31 * result + (target?.hashCode() ?: 0)
temp = java.lang.Double.doubleToLongBits(centerAltitude)
result = 31 * result + (temp xor (temp ushr 32)).toInt()
temp = java.lang.Double.doubleToLongBits(tilt)
result = 31 * result + (temp xor (temp ushr 32)).toInt()
temp = java.lang.Double.doubleToLongBits(roll)
result = 31 * result + (temp xor (temp ushr 32)).toInt()
temp = java.lang.Double.doubleToLongBits(fov)
result = 31 * result + (temp xor (temp ushr 32)).toInt()
temp = java.lang.Double.doubleToLongBits(zoom)
result = 31 * result + (temp xor (temp ushr 32)).toInt()
result = 31 * result + Arrays.hashCode(padding)
@@ -314,7 +329,7 @@ object CameraUpdateFactory {
}
override fun toString(): String {
return ("CameraPositionUpdate{" + "bearing=" + bearing + ", target=" + target + ", tilt=" + tilt + ", zoom=" + zoom + ", padding=" + Arrays.toString(padding) + '}')
return ("CameraPositionUpdate{" + "bearing=" + bearing + ", target=" + target + ", centerAltitude=" + centerAltitude + ", tilt=" + tilt + ", roll=" + roll + ", fov=" + fov + ", zoom=" + zoom + ", padding=" + Arrays.toString(padding) + '}')
}
}

View File

@@ -168,6 +168,11 @@ public class MapLibreConstants {
*/
public static final double MINIMUM_DIRECTION = 0;
/**
* The current default vertical field of view (matches mbgl::util::DEFAULT_FOV)
*/
public static final double DEFAULT_FOV = 36.86989764584402;
/**
* The currently used minimum scale factor to clamp to when a quick zoom gesture occurs
*

View File

@@ -21,9 +21,12 @@
<!--Camera-->
<attr name="maplibre_cameraTargetLat" format="float"/>
<attr name="maplibre_cameraTargetLng" format="float"/>
<attr name="maplibre_cameraCenterAltitude" format="float"/>
<attr name="maplibre_cameraZoom" format="float"/>
<attr name="maplibre_cameraBearing" format="float"/>
<attr name="maplibre_cameraTilt" format="float"/>
<attr name="maplibre_cameraRoll" format="float"/>
<attr name="maplibre_cameraFov" format="float"/>
<!--Zoom-->
<attr name="maplibre_cameraZoomMax" format="float"/>

View File

@@ -85,11 +85,12 @@ class CameraPositionTest : BaseTest() {
fun testToString() {
val latLng = LatLng(1.0, 2.0)
val cameraPosition =
CameraPosition(latLng, 3.0, 4.0, 5.0, doubleArrayOf(0.0, 500.0, 0.0, 0.0))
CameraPosition(latLng, 9.0, 3.0, 4.0, 5.0, 6.0, 7.0, doubleArrayOf(0.0, 500.0, 0.0, 0.0))
Assert.assertEquals(
"toString should match",
"Target: LatLng [latitude=1.0, longitude=2.0, altitude=0.0], Zoom:3.0, " +
"Bearing:5.0, Tilt:4.0, Padding:[0.0, 500.0, 0.0, 0.0]",
"Target: LatLng [latitude=1.0, longitude=2.0, altitude=0.0], " +
"Center Altitude:9.0, Zoom:3.0, Bearing:5.0, Tilt:4.0, Roll:6.0, FOV:7.0, " +
"Padding:[0.0, 500.0, 0.0, 0.0]",
cameraPosition.toString()
)
}
@@ -98,8 +99,8 @@ class CameraPositionTest : BaseTest() {
fun testHashcode() {
val latLng = LatLng(1.0, 2.0)
val cameraPosition =
CameraPosition(latLng, 3.0, 4.0, 5.0, doubleArrayOf(0.0, 500.0, 0.0, 0.0))
Assert.assertEquals("hashCode should match", -420915327, cameraPosition.hashCode().toLong())
CameraPosition(latLng, 9.0, 3.0, 4.0, 5.0, 6.0, 7.0, doubleArrayOf(0.0, 500.0, 0.0, 0.0))
Assert.assertEquals("hashCode should match", -1086105727, cameraPosition.hashCode().toLong())
}
@Test

View File

@@ -100,7 +100,7 @@ public abstract class CameraTest extends BaseTest {
invoke(maplibreMap, (uiController, maplibreMap) -> {
final LatLng moveTarget = new LatLng(1.0000000001, 1.0000000003);
final float moveZoom = 15.5f;
final float moveTilt = 45.5f;
final float moveTilt = 35.5f;
final float moveBearing = 12.5f;
final double[] movePadding = new double[] {0, 500, 350, 1};

View File

@@ -55,9 +55,13 @@ int main(int argc, char* argv[]) {
argumentParser, "directory", "Action journal log directory", {"actionJournalDir"});
args::ValueFlag<double> lonValue(argumentParser, "degrees", "Longitude", {'x', "lon"});
args::ValueFlag<double> latValue(argumentParser, "degrees", "Latitude", {'y', "lat"});
args::ValueFlag<double> altValue(argumentParser, "degrees", "Altitude", {'a', "alt"});
args::ValueFlag<double> fovValue(argumentParser, "degrees", "FOV", {'F', "fov"});
args::ValueFlag<double> zoomValue(argumentParser, "number", "Zoom level", {'z', "zoom"});
args::ValueFlag<double> bearingValue(argumentParser, "degrees", "Bearing", {'b', "bearing"});
args::ValueFlag<double> pitchValue(argumentParser, "degrees", "Pitch", {'p', "pitch"});
args::ValueFlag<double> rollValue(argumentParser, "degrees", "Roll", {'r', "roll"});
args::ValueFlag<double> maxPitchValue(argumentParser, "degrees", "Max Pitch", {'P', "maxPitch"});
try {
argumentParser.ParseCLI(argc, argv);
@@ -79,9 +83,13 @@ int main(int argc, char* argv[]) {
settings.online = !offlineFlag;
if (lonValue) settings.longitude = args::get(lonValue);
if (latValue) settings.latitude = args::get(latValue);
if (altValue) settings.altitude = args::get(altValue);
if (fovValue) settings.fov = args::get(fovValue);
if (maxPitchValue) settings.maxPitch = args::get(maxPitchValue);
if (zoomValue) settings.zoom = args::get(zoomValue);
if (bearingValue) settings.bearing = args::get(bearingValue);
if (pitchValue) settings.pitch = args::get(pitchValue);
if (rollValue) settings.roll = args::get(rollValue);
const bool fullscreen = fullscreenFlag ? args::get(fullscreenFlag) : false;
const bool benchmark = benchmarkFlag ? args::get(benchmarkFlag) : false;
@@ -147,6 +155,8 @@ int main(int argc, char* argv[]) {
clientOptions,
actionJournalOptions);
map.setBounds(mbgl::BoundOptions().withMaxPitch(settings.maxPitch));
backend.setMap(&map);
if (!style.empty() && style.find("://") == std::string::npos) {
@@ -155,9 +165,12 @@ int main(int argc, char* argv[]) {
map.jumpTo(mbgl::CameraOptions()
.withCenter(mbgl::LatLng{settings.latitude, settings.longitude})
.withCenterAltitude(settings.altitude)
.withZoom(settings.zoom)
.withBearing(settings.bearing)
.withPitch(settings.pitch));
.withPitch(settings.pitch)
.withRoll(settings.roll)
.withFov(settings.fov));
map.setDebug(mbgl::MapDebugOptions(settings.debug));
if (testDirValue) view->setTestDirectory(args::get(testDirValue));
@@ -237,15 +250,21 @@ int main(int argc, char* argv[]) {
mbgl::CameraOptions camera = map.getCameraOptions();
settings.latitude = camera.center->latitude();
settings.longitude = camera.center->longitude();
settings.altitude = *camera.centerAltitude;
settings.zoom = *camera.zoom;
settings.bearing = *camera.bearing;
settings.pitch = *camera.pitch;
settings.roll = *camera.roll;
settings.fov = *camera.fov;
settings.debug = mbgl::EnumType(map.getDebug());
settings.save();
mbgl::Log::Info(mbgl::Event::General,
"Exit location: --lat=\"" + std::to_string(settings.latitude) + "\" --lon=\"" +
std::to_string(settings.longitude) + "\" --zoom=\"" + std::to_string(settings.zoom) +
"\" --bearing=\"" + std::to_string(settings.bearing) + "\"");
std::to_string(settings.longitude) + "\" --alt=\"" + std::to_string(settings.altitude) +
"\" --zoom=\"" + std::to_string(settings.zoom) + "\" --bearing=\"" +
std::to_string(settings.bearing) + "\" --roll=\"" + std::to_string(settings.roll) +
"\" --fov=\"" + std::to_string(settings.fov) + "\" --maxPitch=\"" +
std::to_string(settings.maxPitch) + "\"");
view = nullptr;

View File

@@ -1,5 +1,7 @@
#include "settings_json.hpp"
#include <fstream>
#include <mbgl/util/constants.hpp>
#include <mbgl/math/angles.hpp>
namespace mbgl {
@@ -12,9 +14,13 @@ void Settings_JSON::load() {
if (file) {
file >> longitude;
file >> latitude;
file >> altitude;
file >> zoom;
file >> bearing;
file >> pitch;
file >> roll;
file >> fov;
file >> maxPitch;
file >> debug;
file >> online;
}
@@ -25,9 +31,13 @@ void Settings_JSON::save() {
if (file) {
file << longitude << std::endl;
file << latitude << std::endl;
file << altitude << std::endl;
file << zoom << std::endl;
file << bearing << std::endl;
file << pitch << std::endl;
file << roll << std::endl;
file << fov << std::endl;
file << maxPitch << std::endl;
file << debug << std::endl;
file << online << std::endl;
}
@@ -36,9 +46,13 @@ void Settings_JSON::save() {
void Settings_JSON::clear() {
longitude = 0;
latitude = 0;
altitude = 0;
zoom = 0;
bearing = 0;
pitch = 0;
roll = 0;
fov = util::rad2deg(util::DEFAULT_FOV);
maxPitch = util::rad2deg(util::DEFAULT_PITCH_MAX);
debug = 0;
online = true;
}

View File

@@ -1,6 +1,8 @@
#pragma once
#include <mbgl/map/mode.hpp>
#include <mbgl/util/constants.hpp>
#include <mbgl/math/angles.hpp>
namespace mbgl {
@@ -14,9 +16,13 @@ public:
public:
double longitude = 0;
double latitude = 0;
double altitude = 0;
double zoom = 0;
double bearing = 0;
double pitch = 0;
double roll = 0;
double fov = util::rad2deg(mbgl::util::DEFAULT_FOV);
double maxPitch = util::rad2deg(mbgl::util::DEFAULT_PITCH_MAX);
bool axonometric = false;
double xSkew = 0.0;
double ySkew = 1.0;

View File

@@ -155,6 +155,7 @@ struct TestMetadata {
bool ignoreProbing = true;
mbgl::Size size{512u, 512u};
std::optional<float> maxPitch;
float pixelRatio = 1.0f;
double allowed = 0.00015; // diff
std::string description;

View File

@@ -510,6 +510,11 @@ TestMetadata parseTestMetadata(const TestPaths& paths) {
}
}
if (testValue.HasMember("maxPitch")) {
assert(testValue["maxPitch"].IsNumber());
metadata.maxPitch = testValue["maxPitch"].GetFloat();
}
if (testValue.HasMember("pixelRatio")) {
assert(testValue["pixelRatio"].IsNumber());
metadata.pixelRatio = testValue["pixelRatio"].GetFloat();
@@ -618,6 +623,8 @@ const std::string setCenterOp("setCenter");
const std::string setZoomOp("setZoom");
const std::string setBearingOp("setBearing");
const std::string setPitchOp("setPitch");
const std::string setRollOp("setRoll");
const std::string setCenterAltitudeOp("setCenterElevation");
const std::string setFilterOp("setFilter");
const std::string setLayerZoomRangeOp("setLayerZoomRange");
const std::string setLightOp("setLight");
@@ -805,6 +812,24 @@ TestOperations parseTestOperations(TestMetadata& metadata) {
ctx.getMap().jumpTo(mbgl::CameraOptions().withPitch(pitch));
return true;
});
} else if (operationArray[0].GetString() == setRollOp) {
// setRoll
assert(operationArray.Size() >= 2u);
assert(operationArray[1].IsNumber());
double roll = operationArray[1].GetDouble();
result.emplace_back([roll](TestContext& ctx) {
ctx.getMap().jumpTo(mbgl::CameraOptions().withRoll(roll));
return true;
});
} else if (operationArray[0].GetString() == setCenterAltitudeOp) {
// setCenterAltitude
assert(operationArray.Size() >= 2u);
assert(operationArray[1].IsNumber());
double centerAltitude = operationArray[1].GetDouble();
result.emplace_back([centerAltitude](TestContext& ctx) {
ctx.getMap().jumpTo(mbgl::CameraOptions().withCenterAltitude(centerAltitude));
return true;
});
} else if (operationArray[0].GetString() == setFilterOp) {
// setFilter
assert(operationArray.Size() >= 3u);

View File

@@ -43,6 +43,7 @@ extern const std::string setCenterOp;
extern const std::string setZoomOp;
extern const std::string setBearingOp;
extern const std::string setPitchOp;
extern const std::string setRollOp;
extern const std::string setFilterOp;
extern const std::string setLayerZoomRangeOp;
extern const std::string setLightOp;

View File

@@ -704,7 +704,11 @@ TestRunner::Impl::Impl(const TestMetadata& metadata,
.withPixelRatio(metadata.pixelRatio)
.withCrossSourceCollisions(metadata.crossSourceCollisions),
resourceOptions,
clientOptions) {}
clientOptions) {
if (metadata.maxPitch) {
map.setBounds(BoundOptions().withMaxPitch(*metadata.maxPitch));
}
}
TestRunner::Impl::~Impl() {}

View File

@@ -65,6 +65,44 @@ namespace mbgl {
* Steps 3 and 4 are done in the shaders for all labels.
*/
void getTileSkewVectors(const TransformState& state, vec2& vecEast, vec2& vecSouth) {
const double cosRoll = cos(state.getRoll());
const double sinRoll = sin(state.getRoll());
const double cosPitch = cos(state.getPitch());
const double cosBearing = cos(-state.getBearing());
const double sinBearing = sin(-state.getBearing());
vecSouth[0] = -cosBearing * cosPitch * sinRoll - sinBearing * cosRoll;
vecSouth[1] = -sinBearing * cosPitch * sinRoll + cosBearing * cosRoll;
const double vecSouthLen = std::hypot(vecSouth[0], vecSouth[1]);
if (vecSouthLen < 1.0e-9) {
vecSouth = {0, 0};
} else {
vecSouth[0] /= vecSouthLen;
vecSouth[1] /= vecSouthLen;
}
vecEast[0] = cosBearing * cosPitch * cosRoll - sinBearing * sinRoll;
vecEast[1] = sinBearing * cosPitch * cosRoll + cosBearing * sinRoll;
const double vecEastLen = std::hypot(vecEast[0], vecEast[1]);
if (vecEastLen < 1.0e-9) {
vecEast = {0, 0};
} else {
vecEast[0] /= vecEastLen;
vecEast[1] /= vecEastLen;
}
}
mat4 getTileSkewMatrix(const TransformState& state) {
vec2 vecEast, vecSouth;
getTileSkewVectors(state, vecEast, vecSouth);
mat4 m;
matrix::identity(m);
m[0] = vecEast[0];
m[1] = vecEast[1];
m[4] = vecSouth[0];
m[5] = vecSouth[1];
return m;
}
/*
* Returns a matrix for converting from tile units to the correct label coordinate space.
*/
@@ -78,7 +116,9 @@ mat4 getLabelPlaneMatrix(const mat4& posMatrix,
if (pitchWithMap) {
matrix::scale(m, m, 1 / pixelsToTileUnits, 1 / pixelsToTileUnits, 1);
if (!rotateWithMap) {
matrix::rotate_z(m, m, state.getBearing());
mat4 skew = getTileSkewMatrix(state);
matrix::invert(skew, skew);
matrix::multiply(m, m, skew);
}
} else {
matrix::scale(m, m, state.getSize().width / 2.0, -(state.getSize().height / 2.0), 1.0);
@@ -102,7 +142,7 @@ mat4 getGlCoordMatrix(const mat4& posMatrix,
matrix::multiply(m, m, posMatrix);
matrix::scale(m, m, pixelsToTileUnits, pixelsToTileUnits, 1);
if (!rotateWithMap) {
matrix::rotate_z(m, m, -state.getBearing());
matrix::multiply(m, m, getTileSkewMatrix(state));
}
} else {
matrix::scale(m, m, 1, -1, 1);

View File

@@ -62,6 +62,7 @@ void Transform::resize(const Size size) {
double scale{state.getScale()};
double x{state.getX()};
double y{state.getY()};
double z{state.getZ()};
double lat;
double lon;
@@ -80,7 +81,7 @@ void Transform::resize(const Size size) {
return;
}
state.constrain(scale, x, y);
state.setProperties(TransformStateProperties().withScale(scale).withX(x).withY(y));
state.setProperties(TransformStateProperties().withScale(scale).withX(x).withY(y).withZ(z));
observer.onCameraDidChange(MapObserver::CameraChangeMode::Immediate);
}
@@ -125,8 +126,11 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions&
double bearing = camera.bearing ? util::deg2rad(-*camera.bearing) : getBearing();
double pitch = camera.pitch ? util::deg2rad(*camera.pitch) : getPitch();
double fov = camera.fov ? util::deg2rad(*camera.fov) : getFieldOfView();
double centerAlt = camera.centerAltitude.value_or(state.getCenterAltitude());
double roll = camera.roll ? util::deg2rad(*camera.roll) : getRoll();
if (std::isnan(zoom) || std::isnan(bearing) || std::isnan(pitch)) {
if (std::isnan(zoom) || std::isnan(bearing) || std::isnan(pitch) || std::isnan(roll) || std::isnan(fov)) {
if (animation.transitionFinishFn) {
animation.transitionFinishFn();
}
@@ -146,6 +150,7 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions&
startLatLng.unwrapForShortestPath(latLng);
}
}
const double startCenterAlt = state.getCenterAltitude();
const Point<double> startPoint = Projection::project(startLatLng, state.getScale());
const Point<double> endPoint = Projection::project(latLng, state.getScale());
@@ -153,6 +158,7 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions&
// Constrain camera options.
zoom = util::clamp(zoom, state.getMinZoom(), state.getMaxZoom());
pitch = util::clamp(pitch, state.getMinPitch(), state.getMaxPitch());
fov = util::clamp(fov, state.getMinFieldOfView(), state.getMaxFieldOfView());
// Minimize rotation by taking the shorter path around the circle.
bearing = _normalizeAngle(bearing, state.getBearing());
@@ -161,6 +167,8 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions&
const double startZoom = state.getZoom();
const double startBearing = state.getBearing();
const double startPitch = state.getPitch();
const double startRoll = state.getRoll();
const double startFov = state.getFieldOfView();
state.setProperties(TransformStateProperties()
.withPanningInProgress(unwrappedLatLng != startLatLng)
.withScalingInProgress(zoom != startZoom)
@@ -175,6 +183,7 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions&
LatLng frameLatLng = Projection::unproject(framePoint, state.zoomScale(startZoom));
double frameZoom = util::interpolate(startZoom, zoom, t);
state.setLatLngZoom(frameLatLng, frameZoom);
state.setCenterAltitude(util::interpolate(startCenterAlt, centerAlt, t));
if (bearing != startBearing) {
state.setBearing(util::wrap(util::interpolate(startBearing, bearing, t), -pi, pi));
}
@@ -190,6 +199,12 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions&
if (pitch != startPitch || maxPitch < startPitch) {
state.setPitch(std::min(maxPitch, util::interpolate(startPitch, pitch, t)));
}
if (roll != startRoll) {
state.setRoll(util::interpolate(startRoll, roll, t));
}
if (fov != startFov) {
state.setFieldOfView(util::interpolate(startFov, fov, t));
}
},
duration);
}
@@ -212,11 +227,14 @@ void Transform::flyTo(const CameraOptions& inputCamera,
const EdgeInsets& padding = camera.padding.value_or(state.getEdgeInsets());
const LatLng& latLng = camera.center.value_or(getLatLng(LatLng::Unwrapped)).wrapped();
const double centerAlt = camera.centerAltitude.value_or(state.getCenterAltitude());
double bearing = camera.bearing ? util::deg2rad(-*camera.bearing) : getBearing();
double pitch = camera.pitch ? util::deg2rad(*camera.pitch) : getPitch();
double roll = camera.roll ? util::deg2rad(*camera.roll) : getRoll();
double fov = camera.fov ? util::deg2rad(*camera.fov) : getFieldOfView();
if (std::isnan(zoom) || std::isnan(bearing) || std::isnan(pitch) || state.getSize().isEmpty()) {
if (std::isnan(zoom) || std::isnan(bearing) || std::isnan(pitch) || std::isnan(roll) || std::isnan(fov) ||
state.getSize().isEmpty()) {
if (animation.transitionFinishFn) {
animation.transitionFinishFn();
}
@@ -226,6 +244,7 @@ void Transform::flyTo(const CameraOptions& inputCamera,
// Determine endpoints.
LatLng startLatLng = getLatLng(LatLng::Unwrapped).wrapped();
startLatLng.unwrapForShortestPath(latLng);
const double startCenterAlt = state.getCenterAltitude();
const Point<double> startPoint = Projection::project(startLatLng, state.getScale());
const Point<double> endPoint = Projection::project(latLng, state.getScale());
@@ -233,6 +252,7 @@ void Transform::flyTo(const CameraOptions& inputCamera,
// Constrain camera options.
zoom = util::clamp(zoom, state.getMinZoom(), state.getMaxZoom());
pitch = util::clamp(pitch, state.getMinPitch(), state.getMaxPitch());
fov = util::clamp(fov, state.getMinFieldOfView(), state.getMaxFieldOfView());
// Minimize rotation by taking the shorter path around the circle.
bearing = _normalizeAngle(bearing, state.getBearing());
@@ -240,6 +260,8 @@ void Transform::flyTo(const CameraOptions& inputCamera,
const double startZoom = state.scaleZoom(state.getScale());
const double startBearing = state.getBearing();
const double startPitch = state.getPitch();
const double startRoll = state.getRoll();
const double startFov = state.getFieldOfView();
/// w₀: Initial visible span, measured in pixels at the initial scale.
/// Known henceforth as a <i>screenful</i>.
@@ -353,6 +375,7 @@ void Transform::flyTo(const CameraOptions& inputCamera,
// Convert to geographic coordinates and set the new viewpoint.
LatLng frameLatLng = Projection::unproject(framePoint, startScale);
state.setLatLngZoom(frameLatLng, frameZoom);
state.setCenterAltitude(util::interpolate(startCenterAlt, centerAlt, us));
if (bearing != startBearing) {
state.setBearing(util::wrap(util::interpolate(startBearing, bearing, k), -pi, pi));
}
@@ -364,10 +387,15 @@ void Transform::flyTo(const CameraOptions& inputCamera,
util::interpolate(startEdgeInsets.bottom(), padding.bottom(), k),
util::interpolate(startEdgeInsets.right(), padding.right(), k)});
}
double maxPitch = getMaxPitchForEdgeInsets(state.getEdgeInsets());
if (pitch != startPitch || maxPitch < startPitch) {
state.setPitch(std::min(maxPitch, util::interpolate(startPitch, pitch, k)));
if (pitch != startPitch) {
state.setPitch(util::interpolate(startPitch, pitch, k));
}
if (roll != startRoll) {
state.setPitch(util::interpolate(startRoll, roll, k));
}
if (fov != startFov) {
state.setFieldOfView(util::interpolate(startFov, fov, k));
}
},
duration);
@@ -472,6 +500,14 @@ double Transform::getPitch() const {
return state.getPitch();
}
double Transform::getRoll() const {
return state.getRoll();
}
double Transform::getFieldOfView() const {
return state.getFieldOfView();
}
// MARK: - North Orientation
void Transform::setNorthOrientation(NorthOrientation orientation) {
@@ -479,8 +515,9 @@ void Transform::setNorthOrientation(NorthOrientation orientation) {
double scale{state.getScale()};
double x{state.getX()};
double y{state.getY()};
double z{state.getZ()};
state.constrain(scale, x, y);
state.setProperties(TransformStateProperties().withScale(scale).withX(x).withY(y));
state.setProperties(TransformStateProperties().withScale(scale).withX(x).withY(y).withZ(z));
}
NorthOrientation Transform::getNorthOrientation() const {
@@ -494,8 +531,9 @@ void Transform::setConstrainMode(mbgl::ConstrainMode mode) {
double scale{state.getScale()};
double x{state.getX()};
double y{state.getY()};
double z{state.getZ()};
state.constrain(scale, x, y);
state.setProperties(TransformStateProperties().withScale(scale).withX(x).withY(y));
state.setProperties(TransformStateProperties().withScale(scale).withX(x).withY(y).withZ(z));
}
ConstrainMode Transform::getConstrainMode() const {
@@ -676,22 +714,16 @@ LatLng Transform::screenCoordinateToLatLng(const ScreenCoordinate& point, LatLng
double Transform::getMaxPitchForEdgeInsets(const EdgeInsets& insets) const {
double centerOffsetY = 0.5 * (insets.top() - insets.bottom()); // See TransformState::getCenterOffset.
if (centerOffsetY == 0.0) {
return state.getMaxPitch();
}
const auto height = state.getSize().height;
assert(height);
// For details, see description at
// https://github.com/mapbox/mapbox-gl-native/pull/15195 The definition of
// half of TransformState::fov with no inset, is: fov = arctan((height / 2)
// / (height * 1.5)). We use half of fov, as it is field of view above
// perspective center. With inset, this angle changes and
// tangentOfFovAboveCenterAngle = (h/2 + centerOffsetY) / (height
// * 1.5). 1.03 is a bit extra added to prevent parallel ground to viewport
// clipping plane.
const double tangentOfFovAboveCenterAngle = 1.03 * (height / 2.0 + centerOffsetY) / (1.5 * height);
// Half of fov is the field of view above perspective center.
const double tangentOfFovAboveCenterAngle = (0.5 + centerOffsetY / height) * 2.0 * tan(getFieldOfView() / 2.0);
const double fovAboveCenter = std::atan(tangentOfFovAboveCenterAngle);
return pi * 0.5 - fovAboveCenter;
// e.g. Maximum pitch of 60 degrees is when perspective center's offset from
// the top is 84% of screen height.
return state.getMaxPitch() + getFieldOfView() / 2.0 - fovAboveCenter;
}
FreeCameraOptions Transform::getFreeCameraOptions() const {

View File

@@ -91,6 +91,8 @@ public:
// Pitch
double getPitch() const;
double getRoll() const;
double getFieldOfView() const;
// North Orientation
void setNorthOrientation(NorthOrientation);

View File

@@ -15,6 +15,14 @@ using namespace std::numbers;
namespace mbgl {
/*
* The maximum angle to use for the Mercator horizon. This must be less than 90
* to prevent errors in `MercatorTransform::_calcMatrices()`. It shouldn't be too close
* to 90, or the distance to the horizon will become very large, unnecessarily increasing
* the number of tiles needed to render the map.
*/
const double maxMercatorHorizonAngle = util::deg2rad(89.25);
namespace {
LatLng latLngFromMercator(Point<double> mercatorCoordinate, LatLng::WrapMode wrapMode = LatLng::WrapMode::Unwrapped) {
return {util::rad2deg(2 * std::atan(std::exp(pi - mercatorCoordinate.y * 2 * pi)) - (pi / 2)),
@@ -46,15 +54,24 @@ void TransformState::setProperties(const TransformStateProperties& properties) {
if (properties.y) {
setY(*properties.y);
}
if (properties.z) {
setZ(*properties.z);
}
if (properties.scale) {
setScale(*properties.scale);
}
if (properties.bearing) {
setBearing(*properties.bearing);
}
if (properties.fov) {
setFieldOfView(*properties.fov);
}
if (properties.pitch) {
setPitch(*properties.pitch);
}
if (properties.roll) {
setRoll(*properties.roll);
}
if (properties.xSkew) {
setXSkew(*properties.xSkew);
}
@@ -116,6 +133,9 @@ void TransformState::getProjMatrix(mat4& projMatrix, uint16_t nearZ, bool aligne
const double cameraToCenterDistance = getCameraToCenterDistance();
const ScreenCoordinate offset = getCenterOffset();
const double limitedPitch = util::clamp(getPitch(), 0.0, maxMercatorHorizonAngle);
const double cameraToSeaLevelDistance = cameraToCenterDistance + std::abs(z) / std::cos(limitedPitch);
// Find the Z distance from the viewport center point
// [width/2 + offset.x, height/2 + offset.y] to the top edge; to point
// [width/2 + offset.x, 0] in Z units.
@@ -123,12 +143,13 @@ void TransformState::getProjMatrix(mat4& projMatrix, uint16_t nearZ, bool aligne
// (the distance between[width/2, height/2] and [width/2 + 1, height/2])
// See https://github.com/mapbox/mapbox-gl-native/pull/15195 for details.
// See TransformState::fov description: fov = 2 * arctan((height / 2) / (height * 1.5)).
const double tanFovAboveCenter =
((size.height - frustumOffset.top()) * 0.5 + (offset.y - (frustumOffset.top() / 2))) / (size.height * 1.5);
const double tanMultiple = tanFovAboveCenter * std::tan(getPitch());
const double tanFovAboveCenter = (0.5 + (offset.y - frustumOffset.top()) / size.height) * 2.0 *
std::tan(fov / 2.0) *
(std::abs(std::cos(roll)) + std::abs(std::sin(roll)) * size.width / size.height);
const double tanMultiple = util::clamp(tanFovAboveCenter * std::tan(limitedPitch), 0.0, 0.99);
assert(tanMultiple < 1);
// Calculate z distance of the farthest fragment that should be rendered.
const double furthestDistance = cameraToCenterDistance / (1 - tanMultiple);
const double furthestDistance = cameraToSeaLevelDistance / (1 - tanMultiple);
// Add a bit extra to avoid precision problems when a fragment's distance is exactly `furthestDistance`
const double farZ = furthestDistance * 1.01;
@@ -208,13 +229,13 @@ void TransformState::updateCameraState() const {
const double dy = 0.5 * worldSize - y;
// Set camera orientation and move it to a proper distance from the map
camera.setOrientation(getPitch(), getBearing());
camera.setOrientation(getRoll(), getPitch(), getBearing());
const vec3 forward = camera.forward();
const vec3 orbitPosition = {{-forward[0] * cameraToCenterDistance,
-forward[1] * cameraToCenterDistance,
-forward[2] * cameraToCenterDistance}};
vec3 cameraPosition = {{dx + orbitPosition[0], dy + orbitPosition[1], orbitPosition[2]}};
vec3 cameraPosition = {{dx + orbitPosition[0], dy + orbitPosition[1], z + orbitPosition[2]}};
cameraPosition[0] /= worldSize;
cameraPosition[1] /= worldSize;
@@ -230,26 +251,39 @@ void TransformState::updateStateFromCamera() {
const double dx = forward[0];
const double dy = forward[1];
const double dz = forward[2];
assert(position[2] > 0.0 && dz < 0.0);
// Compute bearing and pitch
double newBearing;
double newPitch;
camera.getOrientation(newPitch, newBearing);
double newRoll;
camera.getOrientation(newRoll, newPitch, newBearing);
newPitch = util::clamp(newPitch, minPitch, maxPitch);
// Compute zoom level from the camera altitude
const double centerDistance = getCameraToCenterDistance();
const double zoom = util::log2(centerDistance / (position[2] / std::cos(newPitch) * util::tileSize_D));
const double newScale = util::clamp(std::pow(2.0, zoom), min_scale, max_scale);
double zoom;
double newScale;
double travel;
if (dz < -1.0e-9 && position[2] > 1.0e-9 && newPitch <= maxMercatorHorizonAngle) {
zoom = util::log2(centerDistance / (position[2] / std::cos(newPitch) * util::tileSize_D));
newScale = util::clamp(std::pow(2.0, zoom), min_scale, max_scale);
travel = -position[2] / dz;
} else {
zoom = 14;
newScale = util::clamp(std::pow(2.0, zoom), min_scale, max_scale);
travel = centerDistance / newScale / util::tileSize_D;
}
// Compute center point of the map
const double travel = -position[2] / dz;
const Point<double> mercatorPoint = {position[0] + dx * travel, position[1] + dy * travel};
setLatLngZoom(latLngFromMercator(mercatorPoint), scaleZoom(newScale));
const double mercatorZ = position[2] + dz * travel;
double alt_m = mercatorZ * Projection::getMetersPerPixelAtLatitude(getLatLng().latitude(), 0) * util::tileSize_D;
setCenterAltitude(alt_m);
setBearing(newBearing);
setPitch(newPitch);
setRoll(newRoll);
}
FreeCameraOptions TransformState::getFreeCameraOptions() const {
@@ -291,11 +325,6 @@ bool TransformState::setCameraOrientation(const Quaternion& orientation_) {
const vec3 forward = unitQuat.transform({{0.0, 0.0, -1.0}});
const vec3 up = unitQuat.transform({{0.0, -1.0, 0.0}});
if (up[2] < 0.0) {
// Camera is upside down and not recoverable
return false;
}
const std::optional<Quaternion> updatedOrientation = util::Camera::orientationFromFrame(forward, up);
if (!updatedOrientation) return false;
@@ -443,10 +472,13 @@ void TransformState::setViewportMode(ViewportMode val) {
CameraOptions TransformState::getCameraOptions(const std::optional<EdgeInsets>& padding) const {
return CameraOptions()
.withCenter(getLatLng())
.withCenterAltitude(getCenterAltitude())
.withPadding(padding ? padding : edgeInsets)
.withZoom(getZoom())
.withBearing(util::rad2deg(-bearing))
.withPitch(util::rad2deg(pitch));
.withPitch(util::rad2deg(pitch))
.withRoll(util::rad2deg(roll))
.withFov(util::rad2deg(fov));
}
// MARK: - EdgeInsets
@@ -464,6 +496,10 @@ LatLng TransformState::getLatLng(LatLng::WrapMode wrapMode) const {
return {util::rad2deg(2 * std::atan(std::exp(y / Cc)) - 0.5 * pi), -x / Bc, wrapMode};
}
double TransformState::getCenterAltitude() const {
return z * Projection::getMetersPerPixelAtLatitude(getLatLng().latitude(), getZoom());
}
double TransformState::pixel_x() const {
const double center = (size.width - Projection::worldSize(scale)) / 2;
return center + x;
@@ -554,6 +590,14 @@ double TransformState::getMaxPitch() const {
return maxPitch;
}
double TransformState::getMinFieldOfView() const {
return minFov;
}
double TransformState::getMaxFieldOfView() const {
return maxFov;
}
// MARK: - Scale
double TransformState::getScale() const {
return scale;
@@ -590,6 +634,17 @@ void TransformState::setY(double val) {
}
}
double TransformState::getZ() const {
return z;
}
void TransformState::setZ(double val) {
if (z != val) {
z = val;
requestMatricesUpdate = true;
}
}
// MARK: - Rotation
double TransformState::getBearing() const {
@@ -607,6 +662,24 @@ float TransformState::getFieldOfView() const {
return static_cast<float>(fov);
}
void TransformState::setFieldOfView(double val) {
if (fov != val) {
fov = val;
requestMatricesUpdate = true;
}
}
double TransformState::getRoll() const {
return roll;
}
void TransformState::setRoll(double val) {
if (roll != val) {
roll = val;
requestMatricesUpdate = true;
}
}
float TransformState::getCameraToCenterDistance() const {
return static_cast<float>(0.5 * size.height / std::tan(fov / 2.0));
}
@@ -730,7 +803,7 @@ TileCoordinate TransformState::screenCoordinateToTileCoordinate(const ScreenCoor
double z0 = coord0[2] / w0;
double z1 = coord1[2] / w1;
double t = z0 == z1 ? 0 : (targetZ - z0) / (z1 - z0);
double t = z0 <= z1 ? 0 : (targetZ - z0) / (z1 - z0);
Point<double> p = util::interpolate(p0, p1, t) / scale * static_cast<double>(1 << atZoom);
return {.p = {p.x, p.y}, .z = static_cast<double>(atZoom)};
@@ -957,6 +1030,11 @@ void TransformState::setLatLngZoom(const LatLng& latLng, double zoom) {
setScalePoint(newScale, point);
}
void TransformState::setCenterAltitude(double alt_m) {
z = alt_m / Projection::getMetersPerPixelAtLatitude(getLatLng().latitude(), getZoom());
requestMatricesUpdate = true;
}
void TransformState::setScalePoint(const double newScale, const ScreenCoordinate& point) {
double constrainedScale = newScale;
ScreenCoordinate constrainedPoint = point;

View File

@@ -29,10 +29,18 @@ struct TransformStateProperties {
y = val;
return *this;
}
TransformStateProperties& withZ(const std::optional<double>& val) {
z = val;
return *this;
}
TransformStateProperties& withScale(const std::optional<double>& val) {
scale = val;
return *this;
}
TransformStateProperties& withFov(const std::optional<double>& val) {
fov = val;
return *this;
}
TransformStateProperties& withBearing(const std::optional<double>& val) {
bearing = val;
return *this;
@@ -41,6 +49,10 @@ struct TransformStateProperties {
pitch = val;
return *this;
}
TransformStateProperties& withRoll(const std::optional<double>& val) {
roll = val;
return *this;
}
TransformStateProperties& withXSkew(const std::optional<double>& val) {
xSkew = val;
return *this;
@@ -92,9 +104,12 @@ struct TransformStateProperties {
std::optional<double> x;
std::optional<double> y;
std::optional<double> bearing;
std::optional<double> z;
std::optional<double> fov;
std::optional<double> scale;
std::optional<double> bearing;
std::optional<double> pitch;
std::optional<double> roll;
std::optional<double> xSkew;
std::optional<double> ySkew;
std::optional<bool> axonometric;
@@ -147,6 +162,7 @@ public:
// Position
LatLng getLatLng(LatLng::WrapMode = LatLng::Unwrapped) const;
double getCenterAltitude() const;
double pixel_x() const;
double pixel_y() const;
@@ -164,6 +180,8 @@ public:
void setX(double);
double getY() const;
void setY(double);
double getZ() const;
void setZ(double);
// Bounds
void setLatLngBounds(LatLngBounds);
@@ -176,14 +194,19 @@ public:
double getMinPitch() const;
void setMaxPitch(double);
double getMaxPitch() const;
double getMinFieldOfView() const;
double getMaxFieldOfView() const;
// Rotation
double getBearing() const;
void setBearing(double);
float getFieldOfView() const;
void setFieldOfView(double);
float getCameraToCenterDistance() const;
double getPitch() const;
void setPitch(double);
double getRoll() const;
void setRoll(double);
double getXSkew() const;
void setXSkew(double);
@@ -222,6 +245,7 @@ public:
point on screen. */
void moveLatLng(const LatLng&, const ScreenCoordinate&);
void setLatLngZoom(const LatLng& latLng, double zoom);
void setCenterAltitude(double alt_m);
void constrain(double& scale, double& x, double& y) const;
bool constrainScreen(double& scale_, double& x_, double& y_) const;
@@ -248,7 +272,9 @@ private:
// Limit the amount of pitch
double minPitch = util::PITCH_MIN;
double maxPitch = util::PITCH_MAX;
double maxPitch = util::DEFAULT_PITCH_MAX;
double minFov = util::deg2rad(0.1);
double maxFov = util::deg2rad(150.0);
NorthOrientation orientation = NorthOrientation::Upwards;
@@ -283,15 +309,12 @@ private:
bool gestureInProgress = false;
// map position
double x = 0, y = 0;
double x = 0, y = 0, z = 0;
double bearing = 0;
double scale = 1;
// This fov value is somewhat arbitrary. The altitude of the camera used
// to be defined as 1.5 screen heights above the ground, which was an
// arbitrary choice. This is the fov equivalent to that value calculated
// with: `fov = 2 * arctan((height / 2) / (height * 1.5))`
double fov = 0.6435011087932844;
double fov = util::DEFAULT_FOV;
double pitch = 0.0;
double roll = 0.0;
double xSkew = 0.0;
double ySkew = 1.0;
bool axonometric = false;

View File

@@ -69,6 +69,13 @@ StyleParseResult Parser::parse(const std::string& json) {
}
}
if (document.HasMember("centerAltitude")) {
const JSValue& value = document["centerAltitude"];
if (value.IsNumber()) {
centerAltitude = value.GetDouble();
}
}
if (document.HasMember("zoom")) {
const JSValue& value = document["zoom"];
if (value.IsNumber()) {
@@ -90,6 +97,13 @@ StyleParseResult Parser::parse(const std::string& json) {
}
}
if (document.HasMember("roll")) {
const JSValue& value = document["roll"];
if (value.IsNumber()) {
roll = value.GetDouble();
}
}
if (document.HasMember("transition")) {
parseTransition(document["transition"]);
}

View File

@@ -45,6 +45,8 @@ public:
double zoom = 0;
double bearing = 0;
double pitch = 0;
double roll = 0;
double centerAltitude = 0;
// Statically evaluate layer properties to determine what font stacks are used.
std::set<FontStack> fontStacks() const;

View File

@@ -115,9 +115,11 @@ void Style::Impl::parse(const std::string& json_) {
name = parser.name;
defaultCamera.center = parser.latLng;
defaultCamera.centerAltitude = parser.centerAltitude;
defaultCamera.zoom = parser.zoom;
defaultCamera.bearing = parser.bearing;
defaultCamera.pitch = parser.pitch;
defaultCamera.roll = parser.roll;
setLight(std::make_unique<Light>(parser.light));

View File

@@ -14,20 +14,6 @@ using namespace std::numbers;
namespace mbgl {
namespace {
double vec2Len(const vec2& v) noexcept {
return std::sqrt(v[0] * v[0] + v[1] * v[1]);
};
double vec2Dot(const vec2& a, const vec2& b) noexcept {
return a[0] * b[0] + a[1] * b[1];
};
vec2 vec2Scale(const vec2& v, double s) noexcept {
return vec2{{v[0] * s, v[1] * s}};
};
} // namespace
namespace util {
namespace {
@@ -66,12 +52,13 @@ vec3 toMercator(const LatLng& location, double altitudeMeters) noexcept {
altitudeMeters * pixelsPerMeter / worldSize}};
}
Quaternion orientationFromPitchBearing(double pitch, double bearing) noexcept {
Quaternion orientationFromRollPitchBearing(double roll, double pitch, double bearing) noexcept {
// Both angles have to be negated to achieve CW rotation around the axis of rotation
const Quaternion rotBearing = Quaternion::fromAxisAngle({{0.0, 0.0, 1.0}}, -bearing);
const Quaternion rotPitch = Quaternion::fromAxisAngle({{1.0, 0.0, 0.0}}, -pitch);
Quaternion rotBearing = Quaternion::fromAxisAngle({{0.0, 0.0, 1.0}}, -bearing);
Quaternion rotPitch = Quaternion::fromAxisAngle({{1.0, 0.0, 0.0}}, -pitch);
Quaternion rotRoll = Quaternion::fromAxisAngle({{0.0, 0.0, 1.0}}, roll);
return rotBearing.multiply(rotPitch);
return rotBearing.multiply(rotPitch).multiply(rotRoll);
}
void updateTransform(mat4& transform, const Quaternion& orientation) noexcept {
@@ -170,16 +157,24 @@ vec3 Camera::up() const noexcept {
return {{-column[0], -column[1], -column[2]}};
}
void Camera::getOrientation(double& pitch, double& bearing) const noexcept {
void Camera::getOrientation(double& roll, double& pitch, double& bearing) const noexcept {
const vec3 f = forward();
const vec3 r = right();
const vec3 u = up();
bearing = std::atan2(-r[1], r[0]);
pitch = std::atan2(std::sqrt(f[0] * f[0] + f[1] * f[1]), -f[2]);
const double f01 = std::sqrt(f[0] * f[0] + f[1] * f[1]);
pitch = std::atan2(f01, -f[2]);
if (f01 > 1.0e-3) {
bearing = -std::atan2(f[0], -f[1]);
roll = -std::atan2(r[2], u[2]);
} else {
bearing = std::atan2(-r[1], r[0]);
roll = 0.0;
}
}
void Camera::setOrientation(double pitch, double bearing) noexcept {
orientation = orientationFromPitchBearing(pitch, bearing);
void Camera::setOrientation(double roll, double pitch, double bearing) noexcept {
orientation = orientationFromRollPitchBearing(roll, pitch, bearing);
updateTransform(transform, orientation);
}
@@ -193,32 +188,27 @@ void Camera::setPosition(const vec3& position) noexcept {
}
std::optional<Quaternion> Camera::orientationFromFrame(const vec3& forward, const vec3& up) noexcept {
vec3 upVector = up;
const vec2 xyForward = {{forward[0], forward[1]}};
vec2 xyUp = {{up[0], up[1]}};
const double epsilon = 1e-15;
// Remove roll-component of the resulting orientation by projecting
// the up-vector to the forward vector on xy-plane
if (vec2Len(xyForward) >= epsilon) {
const vec2 xyDir = vec2Scale(xyForward, 1.0 / vec2Len(xyForward));
xyUp = vec2Scale(xyDir, vec2Dot(xyUp, xyDir));
upVector[0] = xyUp[0];
upVector[1] = xyUp[1];
}
const vec3 right = vec3Cross(upVector, forward);
const vec3 right = vec3Cross(up, forward);
if (vec3Length(right) < epsilon) {
return std::nullopt;
}
const double bearing = std::atan2(-right[1], right[0]);
const double pitch = std::atan2(std::sqrt(forward[0] * forward[0] + forward[1] * forward[1]), -forward[2]);
const double f01 = std::sqrt(forward[0] * forward[0] + forward[1] * forward[1]);
const double pitch = std::atan2(f01, -forward[2]);
double bearing;
double roll;
if (f01 > 1.0e-3) {
bearing = -std::atan2(forward[0], -forward[1]);
roll = -std::atan2(right[2], up[2]);
} else {
bearing = std::atan2(-right[1], right[0]);
roll = 0.0;
}
return util::orientationFromPitchBearing(pitch, bearing);
return util::orientationFromRollPitchBearing(roll, pitch, bearing);
}
} // namespace util
@@ -258,14 +248,16 @@ void FreeCameraOptions::lookAtPoint(const LatLng& location, const std::optional<
// Flip z-component of the up vector if it's pointing downwards
up[2] = std::abs(up[2]);
const auto newOrientation = util::Camera::orientationFromFrame(forward, up);
if (newOrientation) {
orientation = newOrientation.value().m;
auto q = util::Camera::orientationFromFrame(forward, up);
if (q) {
orientation = q->m;
}
}
void FreeCameraOptions::setPitchBearing(double pitch, double bearing) noexcept {
orientation = util::orientationFromPitchBearing(util::deg2rad(pitch), util::deg2rad(bearing)).m;
void FreeCameraOptions::setRollPitchBearing(double roll, double pitch, double bearing) noexcept {
orientation =
util::orientationFromRollPitchBearing(util::deg2rad(roll), util::deg2rad(pitch), util::deg2rad(bearing)).m;
}
} // namespace mbgl

View File

@@ -25,14 +25,13 @@ public:
vec3 up() const noexcept;
const Quaternion& getOrientation() const noexcept { return orientation; }
void getOrientation(double& pitch, double& bearing) const noexcept;
void setOrientation(double pitch, double bearing) noexcept;
void getOrientation(double& roll, double& pitch, double& bearing) const noexcept;
void setOrientation(double roll, double pitch, double bearing) noexcept;
void setOrientation(const Quaternion& orientation_) noexcept;
void setPosition(const vec3& position) noexcept;
// Computes orientation using forward and up vectors of the provided
// coordinate frame. Only bearing and pitch components will be used. Does
// not return a value if input is invalid
// coordinate frame. Does not return a value if input is invalid
static std::optional<Quaternion> orientationFromFrame(const vec3& forward, const vec3& up) noexcept;
private:

View File

@@ -916,7 +916,6 @@ TEST(Transform, MinMaxPitch) {
transform.setMaxPitch(70);
transform.jumpTo(CameraOptions().withPitch(70));
ASSERT_DOUBLE_EQ(transform.getState().getMaxPitch(), transform.getPitch());
ASSERT_DOUBLE_EQ(util::deg2rad(60), transform.getPitch());
transform.setMaxPitch(45);
transform.jumpTo(CameraOptions().withPitch(60));
@@ -1012,6 +1011,27 @@ TEST(Transform, FreeCameraOptionsInvalidZ) {
EXPECT_LE(transform.getFreeCameraOptions().position.value()[2], 1.0);
}
TEST(Transform, FreeCameraOptionsHighPitch) {
Transform transform;
transform.resize({100, 100});
transform.setMaxPitch(180.0);
FreeCameraOptions options;
options.position = vec3{{0.1, 0.1, 0.1}};
options.orientation = Quaternion::fromAxisAngle(vec3{{1.0, 0.0, 0.0}}, util::deg2rad(-90.0)).m;
transform.setFreeCameraOptions(options);
EXPECT_DOUBLE_EQ(util::deg2rad(90.0), transform.getState().getPitch());
EXPECT_DOUBLE_EQ(0.0, transform.getState().getBearing());
EXPECT_THAT(transform.getFreeCameraOptions().position.value(), Vec3NearEquals1E5(vec3{{0.1, 0.1, 0.1}}));
options.position = vec3{{0.1, 0.1, 0.1}};
options.orientation = Quaternion::fromAxisAngle(vec3{{1.0, 0.0, 0.0}}, util::deg2rad(-135.0)).m;
transform.setFreeCameraOptions(options);
EXPECT_DOUBLE_EQ(util::deg2rad(135.0), transform.getState().getPitch());
EXPECT_DOUBLE_EQ(0.0, transform.getState().getBearing());
EXPECT_THAT(transform.getFreeCameraOptions().position.value(), Vec3NearEquals1E5(vec3{{0.1, 0.1, 0.1}}));
}
TEST(Transform, FreeCameraOptionsInvalidOrientation) {
// Invalid orientations that cannot be clamped into a valid range
Transform transform;
@@ -1023,14 +1043,21 @@ TEST(Transform, FreeCameraOptionsInvalidOrientation) {
EXPECT_EQ(Quaternion::identity.m, transform.getFreeCameraOptions().orientation);
// Gimbal lock. Both forward and up vectors are on xy-plane
transform.setMaxPitch(180.0);
options.orientation = Quaternion::fromAxisAngle(vec3{{0.0, 1.0, 0.0}}, pi / 2).m;
transform.setFreeCameraOptions(options);
EXPECT_EQ(Quaternion::identity.m, transform.getFreeCameraOptions().orientation);
for (int i = 0; i < 4; i++) {
EXPECT_NEAR(Quaternion::fromEulerAngles(pi / 2, pi / 2, pi / 2).m[i],
transform.getFreeCameraOptions().orientation.value()[i],
1.0e-12);
}
// Camera is upside down
options.orientation = Quaternion::fromAxisAngle(vec3{{1.0, 0.0, 0.0}}, pi / 2 + pi / 4).m;
transform.setFreeCameraOptions(options);
EXPECT_EQ(Quaternion::identity.m, transform.getFreeCameraOptions().orientation);
for (int i = 0; i < 4; i++) {
EXPECT_NEAR(options.orientation.value()[i], transform.getFreeCameraOptions().orientation.value()[i], 1.0e-12);
}
}
TEST(Transform, FreeCameraOptionsSetOrientation) {
@@ -1056,6 +1083,7 @@ TEST(Transform, FreeCameraOptionsSetOrientation) {
transform.setFreeCameraOptions(options);
EXPECT_DOUBLE_EQ(util::deg2rad(-56.0), transform.getState().getBearing());
EXPECT_DOUBLE_EQ(0.0, transform.getState().getPitch());
EXPECT_DOUBLE_EQ(0.0, transform.getState().getRoll());
EXPECT_DOUBLE_EQ(0.0, transform.getState().getX());
EXPECT_NEAR(152.192378, transform.getState().getY(), 1e-6);
@@ -1067,6 +1095,15 @@ TEST(Transform, FreeCameraOptionsSetOrientation) {
EXPECT_DOUBLE_EQ(util::deg2rad(30.0), transform.getState().getPitch());
EXPECT_NEAR(1.308930, transform.getState().getX(), 1e-6);
EXPECT_NEAR(56.813889, transform.getState().getY(), 1e-6);
options.orientation = Quaternion::fromAxisAngle(vec3{{0.0, 0.0, 1.0}}, util::deg2rad(-33.0))
.multiply(Quaternion::fromAxisAngle(vec3{{1.0, 0.0, 0.0}}, util::deg2rad(-22.0)))
.multiply(Quaternion::fromAxisAngle(vec3{{0.0, 0.0, 1.0}}, util::deg2rad(11.0)))
.m;
transform.setFreeCameraOptions(options);
EXPECT_DOUBLE_EQ(util::deg2rad(33.0), transform.getState().getBearing());
EXPECT_DOUBLE_EQ(util::deg2rad(22.0), transform.getState().getPitch());
EXPECT_FLOAT_EQ(util::deg2rad(11.0), transform.getState().getRoll());
}
static std::tuple<vec3, vec3, vec3> rotatedFrame(const std::array<double, 4>& quaternion) {
@@ -1075,21 +1112,6 @@ static std::tuple<vec3, vec3, vec3> rotatedFrame(const std::array<double, 4>& qu
q.transform({{1.0, 0.0, 0.0}}), q.transform({{0.0, -1.0, 0.0}}), q.transform({{0.0, 0.0, -1.0}}));
}
TEST(Transform, FreeCameraOptionsClampPitch) {
Transform transform;
transform.resize({100, 100});
FreeCameraOptions options;
vec3 right, up, forward;
options.orientation = Quaternion::fromAxisAngle(vec3{{1.0, 0.0, 0.0}}, util::deg2rad(-85.0)).m;
transform.setFreeCameraOptions(options);
EXPECT_DOUBLE_EQ(util::PITCH_MAX, transform.getState().getPitch());
std::tie(right, up, forward) = rotatedFrame(transform.getFreeCameraOptions().orientation.value());
EXPECT_THAT(right, Vec3NearEquals1E5(vec3{{1.0, 0.0, 0.0}}));
EXPECT_THAT(up, Vec3NearEquals1E5(vec3{{0, -0.5, 0.866025}}));
EXPECT_THAT(forward, Vec3NearEquals1E5(vec3{{0, -0.866025, -0.5}}));
}
TEST(Transform, FreeCameraOptionsClampToBounds) {
Transform transform;
transform.resize({100, 100});
@@ -1130,29 +1152,6 @@ TEST(Transform, FreeCameraOptionsInvalidState) {
EXPECT_THAT(options.position.value(), Vec3NearEquals1E5(vec3{{0.0, 0.0, 0.0}}));
}
TEST(Transform, FreeCameraOptionsOrientationRoll) {
Transform transform;
FreeCameraOptions options;
transform.resize({100, 100});
const auto orientationWithoutRoll = Quaternion::fromEulerAngles(-pi / 4, 0.0, 0.0);
const auto orientationWithRoll = orientationWithoutRoll.multiply(Quaternion::fromEulerAngles(0.0, 0.0, pi / 4));
options.orientation = orientationWithRoll.m;
transform.setFreeCameraOptions(options);
options = transform.getFreeCameraOptions();
EXPECT_NEAR(options.orientation.value()[0], orientationWithoutRoll.x, 1e-9);
EXPECT_NEAR(options.orientation.value()[1], orientationWithoutRoll.y, 1e-9);
EXPECT_NEAR(options.orientation.value()[2], orientationWithoutRoll.z, 1e-9);
EXPECT_NEAR(options.orientation.value()[3], orientationWithoutRoll.w, 1e-9);
EXPECT_NEAR(util::deg2rad(45.0), transform.getState().getPitch(), 1e-9);
EXPECT_NEAR(0.0, transform.getState().getBearing(), 1e-9);
EXPECT_NEAR(0.0, transform.getState().getX(), 1e-9);
EXPECT_NEAR(150.0, transform.getState().getY(), 1e-9);
}
TEST(Transform, FreeCameraOptionsStateSynchronization) {
Transform transform;
transform.resize({100, 100});
@@ -1172,20 +1171,49 @@ TEST(Transform, FreeCameraOptionsStateSynchronization) {
transform.jumpTo(CameraOptions().withPitch(20.0).withBearing(77.0).withCenter(LatLng{-20.0, 20.0}));
EXPECT_THAT(transform.getFreeCameraOptions().position.value(),
Vec3NearEquals1E5(vec3{{0.457922, 0.57926, 0.275301}}));
// Invalid pitch
transform.jumpTo(CameraOptions().withPitch(-10.0).withBearing(0.0));
std::tie(right, up, forward) = rotatedFrame(transform.getFreeCameraOptions().orientation.value());
EXPECT_THAT(transform.getFreeCameraOptions().position.value(),
Vec3NearEquals1E5(vec3{{0.555556, 0.556719, 0.292969}}));
EXPECT_THAT(right, Vec3NearEquals1E5(vec3{{1.0, 0.0, 0.0}}));
EXPECT_THAT(up, Vec3NearEquals1E5(vec3{{0.0, -1.0, 0.0}}));
EXPECT_THAT(forward, Vec3NearEquals1E5(vec3{{0.0, 0.0, -1.0}}));
transform.jumpTo(CameraOptions().withPitch(85.0).withBearing(0.0).withCenter(LatLng{-80.0, 0.0}));
std::tie(right, up, forward) = rotatedFrame(transform.getFreeCameraOptions().orientation.value());
EXPECT_THAT(transform.getFreeCameraOptions().position.value(), Vec3NearEquals1E5(vec3{{0.5, 1.14146, 0.146484}}));
EXPECT_THAT(right, Vec3NearEquals1E5(vec3{{1.0, 0.0, 0.0}}));
EXPECT_THAT(up, Vec3NearEquals1E5(vec3{{0, -0.5, 0.866025}}));
EXPECT_THAT(forward, Vec3NearEquals1E5(vec3{{0, -0.866025, -0.5}}));
}
TEST(Camera, SetOrientation) {
util::Camera camera;
const double bearing = 0.22;
const double pitch = 0.34;
const double roll = 0.0;
double bearing_ = 0;
double pitch_ = 0;
double roll_ = 0;
camera.setOrientation(roll, pitch, bearing);
camera.getOrientation(roll_, pitch_, bearing_);
EXPECT_NEAR(bearing, bearing_, 1.0e-9);
EXPECT_NEAR(pitch, pitch_, 1.0e-9);
EXPECT_NEAR(roll, roll_, 1.0e-9);
}
TEST(Camera, SetOrientationWithRoll) {
util::Camera camera;
const double bearing = 0.22;
const double pitch = 0.34;
const double roll = 0.45;
double bearing_ = 0;
double pitch_ = 0;
double roll_ = 0;
camera.setOrientation(roll, pitch, bearing);
camera.getOrientation(roll_, pitch_, bearing_);
EXPECT_NEAR(bearing, bearing_, 1.0e-9);
EXPECT_NEAR(pitch, pitch_, 1.0e-9);
EXPECT_NEAR(roll, roll_, 1.0e-9);
}
TEST(Camera, SetOrientationWithRollNoPitch) {
util::Camera camera;
const double bearing = 0.22;
const double pitch = 0.0;
const double roll = 0.45;
double bearing_ = 0;
double pitch_ = 0;
double roll_ = 0;
camera.setOrientation(roll, pitch, bearing);
camera.getOrientation(roll_, pitch_, bearing_);
EXPECT_NEAR(bearing - roll, bearing_, 1.0e-9);
EXPECT_NEAR(pitch, pitch_, 1.0e-9);
EXPECT_NEAR(0.0, roll_, 1.0e-9);
}

View File

@@ -34,6 +34,10 @@ TEST(Style, Properties) {
ASSERT_EQ("", style.getName());
ASSERT_EQ((LatLng{20, 10}), *style.getDefaultCamera().center);
style.loadJSON(R"STYLE({"centerAltitude": 999})STYLE");
ASSERT_EQ("", style.getName());
ASSERT_EQ(999, *style.getDefaultCamera().centerAltitude);
style.loadJSON(R"STYLE({"bearing": 24})STYLE");
ASSERT_EQ("", style.getName());
ASSERT_EQ(LatLng{}, *style.getDefaultCamera().center);
@@ -47,6 +51,10 @@ TEST(Style, Properties) {
ASSERT_EQ("", style.getName());
ASSERT_EQ(60, *style.getDefaultCamera().pitch);
style.loadJSON(R"STYLE({"roll": 99})STYLE");
ASSERT_EQ("", style.getName());
ASSERT_EQ(99, *style.getDefaultCamera().roll);
style.loadJSON(R"STYLE({})STYLE");
ASSERT_EQ(Milliseconds(300), *style.getTransitionOptions().duration);
ASSERT_EQ(std::optional<Duration>{}, style.getTransitionOptions().delay);

View File

@@ -180,8 +180,8 @@ TEST(FreeCameraOptions, LookAtPoint) {
ASSERT_TRUE(options.orientation);
std::tie(right, up, forward) = rotatedFrame(options.orientation.value());
ASSERT_THAT(right, Vec3NearEquals1E7(vec3{{0.0, 1.0, 0.0}}));
ASSERT_THAT(up, Vec3NearEquals1E7(vec3{{0.0, 0.0, 1.0}}));
ASSERT_THAT(right, Vec3NearEquals1E7(vec3{{0.0, 0.9714568644195829, 0.2372162738348117}}));
ASSERT_THAT(up, Vec3NearEquals1E7(vec3{{0.0, -0.23721627383481167, 0.97145686441958301}}));
ASSERT_THAT(forward, Vec3NearEquals1E7(vec3{{1.0, 0.0, 0.0}}));
// Up vector pointing downwards
@@ -226,18 +226,18 @@ TEST(FreeCameraOptions, LookAtPointInvalidInput) {
ASSERT_FALSE(options.orientation);
}
TEST(FreeCameraOptions, SetPitchBearing) {
TEST(FreeCameraOptions, SetRollPitchBearing) {
FreeCameraOptions options;
vec3 right, up, forward;
options.setPitchBearing(0.0, 0.0);
options.setRollPitchBearing(0.0, 0.0, 0.0);
ASSERT_TRUE(options.orientation);
std::tie(right, up, forward) = rotatedFrame(options.orientation.value());
ASSERT_THAT(right, Vec3NearEquals1E7(vec3{{1.0, 0.0, 0.0}}));
ASSERT_THAT(up, Vec3NearEquals1E7(vec3{{0.0, -1.0, 0.0}}));
ASSERT_THAT(forward, Vec3NearEquals1E7(vec3{{0.0, 0.0, -1.0}}));
options.setPitchBearing(0.0, 180.0);
options.setRollPitchBearing(0.0, 0.0, 180.0);
ASSERT_TRUE(options.orientation);
std::tie(right, up, forward) = rotatedFrame(options.orientation.value());
ASSERT_THAT(right, Vec3NearEquals1E7(vec3{{-1.0, 0.0, 0.0}}));
@@ -247,14 +247,14 @@ TEST(FreeCameraOptions, SetPitchBearing) {
const double cos60 = std::cos(util::deg2rad(60.0));
const double sin60 = std::sin(util::deg2rad(60.0));
options.setPitchBearing(60.0, 0.0);
options.setRollPitchBearing(0.0, 60.0, 0.0);
ASSERT_TRUE(options.orientation);
std::tie(right, up, forward) = rotatedFrame(options.orientation.value());
ASSERT_THAT(right, Vec3NearEquals1E7(vec3{{1.0, 0.0, 0.0}}));
ASSERT_THAT(up, Vec3NearEquals1E7(vec3{{0.0, -cos60, sin60}}));
ASSERT_THAT(forward, Vec3NearEquals1E7(vec3{{0.0, -sin60, -cos60}}));
options.setPitchBearing(60.0, -450);
options.setRollPitchBearing(0.0, 60.0, -450);
ASSERT_TRUE(options.orientation);
std::tie(right, up, forward) = rotatedFrame(options.orientation.value());
ASSERT_THAT(right, Vec3NearEquals1E7(vec3{{0.0, 1.0, 0.0}}));

View File

@@ -78,9 +78,8 @@ TEST(TileCover, PitchOverAllowedByContentInsets) {
// Top padding of 376 leads to capped pitch. See Transform::getMaxPitchForEdgeInsets.
EXPECT_LE(transform.getPitch() + 0.001, util::deg2rad(60));
EXPECT_EQ(
(std::vector<OverscaledTileID>{{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));
EXPECT_EQ((std::vector<OverscaledTileID>{{3, 4, 3}, {3, 3, 3}, {3, 4, 4}, {3, 3, 4}}),
util::tileCover({transform.getState()}, 3));
}
TEST(TileCover, PitchWithLargerResultSet) {
@@ -102,17 +101,17 @@ TEST(TileCover, PitchWithLargerResultSet) {
EXPECT_EQ((std::vector<OverscaledTileID>{{5, 15, 16},
{5, 15, 17},
{5, 14, 16},
{5, 14, 17},
{5, 16, 16},
{5, 16, 17},
{5, 15, 15},
{5, 14, 17},
{5, 16, 17},
{5, 14, 15},
{5, 15, 18},
{5, 14, 18},
{5, 16, 15},
{5, 15, 18},
{5, 13, 16},
{5, 13, 17},
{5, 14, 18},
{5, 16, 18},
{5, 13, 17},
{5, 13, 18},
{5, 15, 19}}),
(std::vector<OverscaledTileID>{cover.begin(), cover.begin() + 16}));