diff --git a/bin/render.cpp b/bin/render.cpp index 396742cff..0af2a340a 100644 --- a/bin/render.cpp +++ b/bin/render.cpp @@ -39,9 +39,11 @@ int main(int argc, char* argv[]) { args::ValueFlag zoomValue(centerGroup, "number", "Zoom level", {'z', "zoom"}); args::ValueFlag lonValue(centerGroup, "degrees", "Longitude", {'x', "lon"}); args::ValueFlag latValue(centerGroup, "degrees", "Latitude", {'y', "lat"}); - + args::ValueFlag altValue(argumentParser, "degrees", "Altitude", {'A', "alt"}); + args::ValueFlag fovValue(argumentParser, "degrees", "FOV", {'f', "fov"}); args::ValueFlag bearingValue(argumentParser, "degrees", "Bearing", {'b', "bearing"}); args::ValueFlag pitchValue(argumentParser, "degrees", "Pitch", {'p', "pitch"}); + args::ValueFlag rollValue(argumentParser, "degrees", "Roll", {'R', "roll"}); args::ValueFlag widthValue(argumentParser, "pixels", "Image width", {'w', "width"}); args::ValueFlag 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) { diff --git a/include/mbgl/map/camera.hpp b/include/mbgl/map/camera.hpp index 3bdacf5e8..5357647b3 100644 --- a/include/mbgl/map/camera.hpp +++ b/include/mbgl/map/camera.hpp @@ -21,6 +21,10 @@ struct CameraOptions { center = o; return *this; } + CameraOptions& withCenterAltitude(const std::optional& o) { + centerAltitude = o; + return *this; + } CameraOptions& withPadding(const std::optional& p) { padding = p; return *this; @@ -41,10 +45,21 @@ struct CameraOptions { pitch = o; return *this; } + CameraOptions& withRoll(const std::optional& o) { + roll = o; + return *this; + } + CameraOptions& withFov(const std::optional& o) { + fov = o; + return *this; + } /** Coordinate at the center of the map. */ std::optional center; + /** Altitude of the center of the map, in meters above sea level. */ + std::optional centerAltitude; + /** Padding around the interior of the view that affects the frame of reference for `center`. */ std::optional 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 pitch; + + /** Camera roll, measured in degrees. */ + std::optional roll; + + /** Camera vertical field of view, measured in degrees. */ + std::optional 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 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& 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 diff --git a/include/mbgl/util/constants.hpp b/include/mbgl/util/constants.hpp index 847c72a25..4a95e82ff 100644 --- a/include/mbgl/util/constants.hpp +++ b/include/mbgl/util/constants.hpp @@ -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; diff --git a/metrics/integration/render-tests/icon-roll-alignment/auto-rotation-alignment-map/expected.png b/metrics/integration/render-tests/icon-roll-alignment/auto-rotation-alignment-map/expected.png new file mode 100644 index 000000000..bc9261747 Binary files /dev/null and b/metrics/integration/render-tests/icon-roll-alignment/auto-rotation-alignment-map/expected.png differ diff --git a/metrics/integration/render-tests/icon-roll-alignment/auto-rotation-alignment-map/style.json b/metrics/integration/render-tests/icon-roll-alignment/auto-rotation-alignment-map/style.json new file mode 100644 index 000000000..7af8e5714 --- /dev/null +++ b/metrics/integration/render-tests/icon-roll-alignment/auto-rotation-alignment-map/style.json @@ -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" + } + } + ] +} diff --git a/metrics/integration/render-tests/icon-roll-alignment/auto-rotation-alignment-viewport/expected.png b/metrics/integration/render-tests/icon-roll-alignment/auto-rotation-alignment-viewport/expected.png new file mode 100644 index 000000000..c38cf492d Binary files /dev/null and b/metrics/integration/render-tests/icon-roll-alignment/auto-rotation-alignment-viewport/expected.png differ diff --git a/metrics/integration/render-tests/icon-roll-alignment/auto-rotation-alignment-viewport/style.json b/metrics/integration/render-tests/icon-roll-alignment/auto-rotation-alignment-viewport/style.json new file mode 100644 index 000000000..f181d9557 --- /dev/null +++ b/metrics/integration/render-tests/icon-roll-alignment/auto-rotation-alignment-viewport/style.json @@ -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" + } + } + ] +} diff --git a/metrics/integration/render-tests/icon-roll-alignment/map-rotation-alignment-viewport/expected.png b/metrics/integration/render-tests/icon-roll-alignment/map-rotation-alignment-viewport/expected.png new file mode 100644 index 000000000..7fb1bfeb8 Binary files /dev/null and b/metrics/integration/render-tests/icon-roll-alignment/map-rotation-alignment-viewport/expected.png differ diff --git a/metrics/integration/render-tests/icon-roll-alignment/map-rotation-alignment-viewport/style.json b/metrics/integration/render-tests/icon-roll-alignment/map-rotation-alignment-viewport/style.json new file mode 100644 index 000000000..1bc7f7b2b --- /dev/null +++ b/metrics/integration/render-tests/icon-roll-alignment/map-rotation-alignment-viewport/style.json @@ -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" + } + } + ] +} diff --git a/metrics/integration/render-tests/icon-roll-alignment/viewport-rotation-alignment-map/expected.png b/metrics/integration/render-tests/icon-roll-alignment/viewport-rotation-alignment-map/expected.png new file mode 100644 index 000000000..a22865b20 Binary files /dev/null and b/metrics/integration/render-tests/icon-roll-alignment/viewport-rotation-alignment-map/expected.png differ diff --git a/metrics/integration/render-tests/icon-roll-alignment/viewport-rotation-alignment-map/style.json b/metrics/integration/render-tests/icon-roll-alignment/viewport-rotation-alignment-map/style.json new file mode 100644 index 000000000..9007373d3 --- /dev/null +++ b/metrics/integration/render-tests/icon-roll-alignment/viewport-rotation-alignment-map/style.json @@ -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" + } + } + ] +} diff --git a/metrics/integration/render-tests/text-keep-upright/line-placement-true-roll-pitch-bearing/expected.png b/metrics/integration/render-tests/text-keep-upright/line-placement-true-roll-pitch-bearing/expected.png new file mode 100644 index 000000000..fa4a5a274 Binary files /dev/null and b/metrics/integration/render-tests/text-keep-upright/line-placement-true-roll-pitch-bearing/expected.png differ diff --git a/metrics/integration/render-tests/text-keep-upright/line-placement-true-roll-pitch-bearing/style.json b/metrics/integration/render-tests/text-keep-upright/line-placement-true-roll-pitch-bearing/style.json new file mode 100644 index 000000000..314c5308e --- /dev/null +++ b/metrics/integration/render-tests/text-keep-upright/line-placement-true-roll-pitch-bearing/style.json @@ -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 + } + } + ] +} diff --git a/metrics/integration/render-tests/text-keep-upright/line-placement-true-rolled/expected.png b/metrics/integration/render-tests/text-keep-upright/line-placement-true-rolled/expected.png new file mode 100644 index 000000000..db0c4f274 Binary files /dev/null and b/metrics/integration/render-tests/text-keep-upright/line-placement-true-rolled/expected.png differ diff --git a/metrics/integration/render-tests/text-keep-upright/line-placement-true-rolled/style.json b/metrics/integration/render-tests/text-keep-upright/line-placement-true-rolled/style.json new file mode 100644 index 000000000..0767dc811 --- /dev/null +++ b/metrics/integration/render-tests/text-keep-upright/line-placement-true-rolled/style.json @@ -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 + } + } + ] +} diff --git a/metrics/integration/render-tests/text-pitch-scaling/line-half-roll/expected.png b/metrics/integration/render-tests/text-pitch-scaling/line-half-roll/expected.png new file mode 100644 index 000000000..35b29f941 Binary files /dev/null and b/metrics/integration/render-tests/text-pitch-scaling/line-half-roll/expected.png differ diff --git a/metrics/integration/render-tests/text-pitch-scaling/line-half-roll/style.json b/metrics/integration/render-tests/text-pitch-scaling/line-half-roll/style.json new file mode 100644 index 000000000..b8efaa4e1 --- /dev/null +++ b/metrics/integration/render-tests/text-pitch-scaling/line-half-roll/style.json @@ -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 + } + } + ] +} diff --git a/metrics/integration/render-tests/text-roll-alignment/auto-symbol-placement-line/expected.png b/metrics/integration/render-tests/text-roll-alignment/auto-symbol-placement-line/expected.png new file mode 100644 index 000000000..131832bbf Binary files /dev/null and b/metrics/integration/render-tests/text-roll-alignment/auto-symbol-placement-line/expected.png differ diff --git a/metrics/integration/render-tests/text-roll-alignment/auto-symbol-placement-line/style.json b/metrics/integration/render-tests/text-roll-alignment/auto-symbol-placement-line/style.json new file mode 100644 index 000000000..4aad9f8d6 --- /dev/null +++ b/metrics/integration/render-tests/text-roll-alignment/auto-symbol-placement-line/style.json @@ -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" + ] + } + } + ] +} diff --git a/metrics/integration/render-tests/text-roll-alignment/auto-symbol-placement-point/expected.png b/metrics/integration/render-tests/text-roll-alignment/auto-symbol-placement-point/expected.png new file mode 100644 index 000000000..b9e732c1b Binary files /dev/null and b/metrics/integration/render-tests/text-roll-alignment/auto-symbol-placement-point/expected.png differ diff --git a/metrics/integration/render-tests/text-roll-alignment/auto-symbol-placement-point/style.json b/metrics/integration/render-tests/text-roll-alignment/auto-symbol-placement-point/style.json new file mode 100644 index 000000000..7255383f2 --- /dev/null +++ b/metrics/integration/render-tests/text-roll-alignment/auto-symbol-placement-point/style.json @@ -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" + ] + } + } + ] +} diff --git a/metrics/integration/render-tests/text-roll-alignment/map-symbol-placement-line/expected.png b/metrics/integration/render-tests/text-roll-alignment/map-symbol-placement-line/expected.png new file mode 100644 index 000000000..131832bbf Binary files /dev/null and b/metrics/integration/render-tests/text-roll-alignment/map-symbol-placement-line/expected.png differ diff --git a/metrics/integration/render-tests/text-roll-alignment/map-symbol-placement-line/style.json b/metrics/integration/render-tests/text-roll-alignment/map-symbol-placement-line/style.json new file mode 100644 index 000000000..46836d9ae --- /dev/null +++ b/metrics/integration/render-tests/text-roll-alignment/map-symbol-placement-line/style.json @@ -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" + ] + } + } + ] +} diff --git a/metrics/integration/render-tests/text-roll-alignment/map-symbol-placement-point/expected.png b/metrics/integration/render-tests/text-roll-alignment/map-symbol-placement-point/expected.png new file mode 100644 index 000000000..a95f047f9 Binary files /dev/null and b/metrics/integration/render-tests/text-roll-alignment/map-symbol-placement-point/expected.png differ diff --git a/metrics/integration/render-tests/text-roll-alignment/map-symbol-placement-point/style.json b/metrics/integration/render-tests/text-roll-alignment/map-symbol-placement-point/style.json new file mode 100644 index 000000000..12f63e83e --- /dev/null +++ b/metrics/integration/render-tests/text-roll-alignment/map-symbol-placement-point/style.json @@ -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" + ] + } + } + ] +} diff --git a/metrics/integration/render-tests/text-roll-alignment/viewport-symbol-placement-line/expected.png b/metrics/integration/render-tests/text-roll-alignment/viewport-symbol-placement-line/expected.png new file mode 100644 index 000000000..3602942a5 Binary files /dev/null and b/metrics/integration/render-tests/text-roll-alignment/viewport-symbol-placement-line/expected.png differ diff --git a/metrics/integration/render-tests/text-roll-alignment/viewport-symbol-placement-line/style.json b/metrics/integration/render-tests/text-roll-alignment/viewport-symbol-placement-line/style.json new file mode 100644 index 000000000..a895f27fb --- /dev/null +++ b/metrics/integration/render-tests/text-roll-alignment/viewport-symbol-placement-line/style.json @@ -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" + ] + } + } + ] +} diff --git a/metrics/integration/render-tests/text-roll-alignment/viewport-symbol-placement-point/expected.png b/metrics/integration/render-tests/text-roll-alignment/viewport-symbol-placement-point/expected.png new file mode 100644 index 000000000..b9e732c1b Binary files /dev/null and b/metrics/integration/render-tests/text-roll-alignment/viewport-symbol-placement-point/expected.png differ diff --git a/metrics/integration/render-tests/text-roll-alignment/viewport-symbol-placement-point/style.json b/metrics/integration/render-tests/text-roll-alignment/viewport-symbol-placement-point/style.json new file mode 100644 index 000000000..e5a3a7b8f --- /dev/null +++ b/metrics/integration/render-tests/text-roll-alignment/viewport-symbol-placement-point/style.json @@ -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" + ] + } + } + ] +} diff --git a/platform/android/MapLibreAndroid/src/cpp/map/camera_position.cpp b/platform/android/MapLibreAndroid/src/cpp/map/camera_position.cpp index 2d346562d..b0e3bcce8 100644 --- a/platform/android/MapLibreAndroid/src/cpp/map/camera_position.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/map/camera_position.cpp @@ -1,5 +1,7 @@ #include "camera_position.hpp" #include "../geometry/lat_lng.hpp" +#include +#include namespace mbgl { namespace android { @@ -8,12 +10,19 @@ jni::Local> CameraPosition::New(jni::JNIEnv& env, mbgl::CameraOptions options, float pixelRatio) { static auto& javaClass = jni::Class::Singleton(env); - static auto constructor = - javaClass.GetConstructor, double, double, double, jni::Array>(env); + static auto constructor = javaClass.GetConstructor, + double, + double, + double, + double, + double, + double, + jni::Array>(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> 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 paddingVect; auto insets = options.padding.value_or(EdgeInsets{0, 0, 0, 0}); @@ -37,8 +48,16 @@ jni::Local> CameraPosition::New(jni::JNIEnv& env, paddingVect.push_back(insets.bottom() * pixelRatio); padding.SetRegion>(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(env, "tilt"); static auto zoom = javaClass.GetField(env, "zoom"); static auto paddingField = javaClass.GetField>(env, "padding"); + static auto centerAltitude = javaClass.GetField(env, "centerAltitude"); + static auto roll = javaClass.GetField(env, "roll"); + static auto fov = javaClass.GetField(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) { diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/camera/CameraPosition.kt b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/camera/CameraPosition.kt index 9a9c7fb72..d07a3d18c 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/camera/CameraPosition.kt +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/camera/CameraPosition.kt @@ -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 { diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/camera/CameraUpdateFactory.kt b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/camera/CameraUpdateFactory.kt index c14e1e8e7..4eb5ceb1f 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/camera/CameraUpdateFactory.kt +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/camera/CameraUpdateFactory.kt @@ -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) + '}') } } diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/constants/MapLibreConstants.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/constants/MapLibreConstants.java index 1b3778989..10173ca64 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/constants/MapLibreConstants.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/constants/MapLibreConstants.java @@ -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 * diff --git a/platform/android/MapLibreAndroid/src/main/res/values/attrs.xml b/platform/android/MapLibreAndroid/src/main/res/values/attrs.xml index eb367bd91..e38086bdb 100644 --- a/platform/android/MapLibreAndroid/src/main/res/values/attrs.xml +++ b/platform/android/MapLibreAndroid/src/main/res/values/attrs.xml @@ -21,9 +21,12 @@ + + + diff --git a/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/camera/CameraPositionTest.kt b/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/camera/CameraPositionTest.kt index 9fb17a7fe..2847ddd92 100644 --- a/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/camera/CameraPositionTest.kt +++ b/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/camera/CameraPositionTest.kt @@ -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 diff --git a/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/testapp/camera/CameraTest.java b/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/testapp/camera/CameraTest.java index 15dff27e7..b67e9c1f7 100644 --- a/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/testapp/camera/CameraTest.java +++ b/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/testapp/camera/CameraTest.java @@ -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}; diff --git a/platform/glfw/main.cpp b/platform/glfw/main.cpp index eba3a2bca..4b81a0bc0 100644 --- a/platform/glfw/main.cpp +++ b/platform/glfw/main.cpp @@ -55,9 +55,13 @@ int main(int argc, char* argv[]) { argumentParser, "directory", "Action journal log directory", {"actionJournalDir"}); args::ValueFlag lonValue(argumentParser, "degrees", "Longitude", {'x', "lon"}); args::ValueFlag latValue(argumentParser, "degrees", "Latitude", {'y', "lat"}); + args::ValueFlag altValue(argumentParser, "degrees", "Altitude", {'a', "alt"}); + args::ValueFlag fovValue(argumentParser, "degrees", "FOV", {'F', "fov"}); args::ValueFlag zoomValue(argumentParser, "number", "Zoom level", {'z', "zoom"}); args::ValueFlag bearingValue(argumentParser, "degrees", "Bearing", {'b', "bearing"}); args::ValueFlag pitchValue(argumentParser, "degrees", "Pitch", {'p', "pitch"}); + args::ValueFlag rollValue(argumentParser, "degrees", "Roll", {'r', "roll"}); + args::ValueFlag 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; diff --git a/platform/glfw/settings_json.cpp b/platform/glfw/settings_json.cpp index 1355e16f2..fe369506b 100644 --- a/platform/glfw/settings_json.cpp +++ b/platform/glfw/settings_json.cpp @@ -1,5 +1,7 @@ #include "settings_json.hpp" #include +#include +#include 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; } diff --git a/platform/glfw/settings_json.hpp b/platform/glfw/settings_json.hpp index c89accb8a..93ac8897f 100644 --- a/platform/glfw/settings_json.hpp +++ b/platform/glfw/settings_json.hpp @@ -1,6 +1,8 @@ #pragma once #include +#include +#include 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; diff --git a/render-test/metadata.hpp b/render-test/metadata.hpp index 4eb48caf7..37efa7f0c 100644 --- a/render-test/metadata.hpp +++ b/render-test/metadata.hpp @@ -155,6 +155,7 @@ struct TestMetadata { bool ignoreProbing = true; mbgl::Size size{512u, 512u}; + std::optional maxPitch; float pixelRatio = 1.0f; double allowed = 0.00015; // diff std::string description; diff --git a/render-test/parser.cpp b/render-test/parser.cpp index 1bd2ad138..ca07021d8 100644 --- a/render-test/parser.cpp +++ b/render-test/parser.cpp @@ -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); diff --git a/render-test/parser.hpp b/render-test/parser.hpp index a54af3434..61a6ba847 100644 --- a/render-test/parser.hpp +++ b/render-test/parser.hpp @@ -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; diff --git a/render-test/runner.cpp b/render-test/runner.cpp index 9b1bb36b3..c476751fa 100644 --- a/render-test/runner.cpp +++ b/render-test/runner.cpp @@ -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() {} diff --git a/src/mbgl/layout/symbol_projection.cpp b/src/mbgl/layout/symbol_projection.cpp index 6633b81e8..830f57417 100644 --- a/src/mbgl/layout/symbol_projection.cpp +++ b/src/mbgl/layout/symbol_projection.cpp @@ -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); diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index 8b641267c..c24e6eb9a 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -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 startPoint = Projection::project(startLatLng, state.getScale()); const Point 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 startPoint = Projection::project(startLatLng, state.getScale()); const Point 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 screenful. @@ -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 { diff --git a/src/mbgl/map/transform.hpp b/src/mbgl/map/transform.hpp index da72d69f7..812218e40 100644 --- a/src/mbgl/map/transform.hpp +++ b/src/mbgl/map/transform.hpp @@ -91,6 +91,8 @@ public: // Pitch double getPitch() const; + double getRoll() const; + double getFieldOfView() const; // North Orientation void setNorthOrientation(NorthOrientation); diff --git a/src/mbgl/map/transform_state.cpp b/src/mbgl/map/transform_state.cpp index add4ea3ae..73246f267 100644 --- a/src/mbgl/map/transform_state.cpp +++ b/src/mbgl/map/transform_state.cpp @@ -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 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 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 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& 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(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(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 p = util::interpolate(p0, p1, t) / scale * static_cast(1 << atZoom); return {.p = {p.x, p.y}, .z = static_cast(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; diff --git a/src/mbgl/map/transform_state.hpp b/src/mbgl/map/transform_state.hpp index c7d75e5e7..8b398af47 100644 --- a/src/mbgl/map/transform_state.hpp +++ b/src/mbgl/map/transform_state.hpp @@ -29,10 +29,18 @@ struct TransformStateProperties { y = val; return *this; } + TransformStateProperties& withZ(const std::optional& val) { + z = val; + return *this; + } TransformStateProperties& withScale(const std::optional& val) { scale = val; return *this; } + TransformStateProperties& withFov(const std::optional& val) { + fov = val; + return *this; + } TransformStateProperties& withBearing(const std::optional& val) { bearing = val; return *this; @@ -41,6 +49,10 @@ struct TransformStateProperties { pitch = val; return *this; } + TransformStateProperties& withRoll(const std::optional& val) { + roll = val; + return *this; + } TransformStateProperties& withXSkew(const std::optional& val) { xSkew = val; return *this; @@ -92,9 +104,12 @@ struct TransformStateProperties { std::optional x; std::optional y; - std::optional bearing; + std::optional z; + std::optional fov; std::optional scale; + std::optional bearing; std::optional pitch; + std::optional roll; std::optional xSkew; std::optional ySkew; std::optional 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; diff --git a/src/mbgl/style/parser.cpp b/src/mbgl/style/parser.cpp index cdae2e600..ce23f4068 100644 --- a/src/mbgl/style/parser.cpp +++ b/src/mbgl/style/parser.cpp @@ -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"]); } diff --git a/src/mbgl/style/parser.hpp b/src/mbgl/style/parser.hpp index 7aef89720..628ec3f3f 100644 --- a/src/mbgl/style/parser.hpp +++ b/src/mbgl/style/parser.hpp @@ -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 fontStacks() const; diff --git a/src/mbgl/style/style_impl.cpp b/src/mbgl/style/style_impl.cpp index 88d65d0ed..24bd8fc8f 100644 --- a/src/mbgl/style/style_impl.cpp +++ b/src/mbgl/style/style_impl.cpp @@ -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(parser.light)); diff --git a/src/mbgl/util/camera.cpp b/src/mbgl/util/camera.cpp index 5ef8de7d7..1d72db103 100644 --- a/src/mbgl/util/camera.cpp +++ b/src/mbgl/util/camera.cpp @@ -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 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 diff --git a/src/mbgl/util/camera.hpp b/src/mbgl/util/camera.hpp index d17e95bcf..d5119dc48 100644 --- a/src/mbgl/util/camera.hpp +++ b/src/mbgl/util/camera.hpp @@ -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 orientationFromFrame(const vec3& forward, const vec3& up) noexcept; private: diff --git a/test/map/transform.test.cpp b/test/map/transform.test.cpp index e98b82c7d..2452a8d57 100644 --- a/test/map/transform.test.cpp +++ b/test/map/transform.test.cpp @@ -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 rotatedFrame(const std::array& quaternion) { @@ -1075,21 +1112,6 @@ static std::tuple rotatedFrame(const std::array& 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); } diff --git a/test/style/style.test.cpp b/test/style/style.test.cpp index 34ac3a5fc..1f5020981 100644 --- a/test/style/style.test.cpp +++ b/test/style/style.test.cpp @@ -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{}, style.getTransitionOptions().delay); diff --git a/test/util/camera.test.cpp b/test/util/camera.test.cpp index 7cb62b87e..f9c2aed1b 100644 --- a/test/util/camera.test.cpp +++ b/test/util/camera.test.cpp @@ -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}})); diff --git a/test/util/tile_cover.test.cpp b/test/util/tile_cover.test.cpp index 7d1f604c5..bf6fda78f 100644 --- a/test/util/tile_cover.test.cpp +++ b/test/util/tile_cover.test.cpp @@ -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{{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{{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{{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{cover.begin(), cover.begin() + 16}));