Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Constrain camera to maximum bounds #2475

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d86c823
Merge pull request #1 from maplibre/main
christian-boks Jun 4, 2024
5b16943
Changes required to constrain the camera to the specified bounds.
christian-boks Jun 4, 2024
0d6a54a
Merge branch 'main' of https://github.com/christian-boks/maplibre-native
christian-boks Jun 4, 2024
c9d83a7
clang-format changes
christian-boks Jun 5, 2024
3ca26ff
Add example to Swift App
louwers Jun 6, 2024
9d0e4a5
Fix typo
louwers Jun 6, 2024
b59ec39
Fix clang compile errors
christian-boks Jun 6, 2024
aeaefc5
Merge branch 'main' of https://github.com/christian-boks/maplibre-native
christian-boks Jun 6, 2024
87d8940
Added new ConstrainMode enum entry to make the changes backwards-comp…
christian-boks Jun 8, 2024
3582ede
Added tests and fixed issue
christian-boks Jun 11, 2024
272183e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 11, 2024
ad9d0dd
Merge branch 'main' into main
christian-boks Jun 14, 2024
c6f3dd2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 14, 2024
c7f8c7a
Renamed new ConstrainMode to Screen
christian-boks Jun 21, 2024
42ee8a8
Merge branch 'main' into main
christian-boks Jun 21, 2024
13ce06d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 21, 2024
875e40d
Renamed MaximumBounds to MaximumScreenBounds and added some more comm…
christian-boks Jun 21, 2024
40c65b0
Merge branch 'main' of https://github.com/christian-boks/maplibre-native
christian-boks Jun 21, 2024
6d45620
Added code to handle constraining screen to bounds because of a resiz…
christian-boks Jul 2, 2024
fc29ce5
Merge branch 'main' into main
christian-boks Jul 2, 2024
c9f3a91
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 2, 2024
c31bb14
Merge branch 'main' into main
christian-boks Jul 12, 2024
7ff1380
Merge branch 'main' into main
christian-boks Jul 29, 2024
186d740
Merge branch 'main' into main
christian-boks Sep 19, 2024
c7350cb
Merge branch 'main' into main
louwers Sep 19, 2024
4e6006e
Merge branch 'main' into main
louwers Nov 14, 2024
65be132
Merge branch 'main' into main
christian-boks Nov 15, 2024
83b708a
Added test of the transform::resize method
christian-boks Nov 15, 2024
8c24d78
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions include/mbgl/map/bound_options.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace mbgl {
*/
struct BoundOptions {
/// Sets the latitude and longitude bounds to which the camera center are constrained
/// If ConstrainMode is set to Screen these bounds describe what can be shown on screen.
BoundOptions& withLatLngBounds(LatLngBounds b) {
bounds = b;
return *this;
Expand All @@ -38,6 +39,7 @@ struct BoundOptions {
}

/// Constrain the center of the camera to be within these bounds.
/// If ConstrainMode is set to Screen these bounds describe what can be shown on screen.
std::optional<LatLngBounds> bounds;

/// Maximum zoom level allowed.
Expand Down
5 changes: 3 additions & 2 deletions include/mbgl/map/mode.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ enum class MapMode : EnumType {
Tile ///< a once-off still image of a single tile
};

/// We can choose to constrain the map both horizontally or vertically, or only
/// vertically e.g. while panning.
/// We can choose to constrain the map both horizontally or vertically, only
/// vertically e.g. while panning, or screen to the specified bounds.
enum class ConstrainMode : EnumType {
None,
HeightOnly,
WidthAndHeight,
Screen,
};

/// Satisfies embedding platforms that requires the viewport coordinate systems
Expand Down
3 changes: 3 additions & 0 deletions platform/ios/app-swift/Sources/MapLibreNavigationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ struct MapLibreNavigationView: View {
NavigationLink("BlockingGesturesExample") {
BlockingGesturesExample()
}
NavigationLink("MaximumScreenBoundsExample") {
MaximumScreenBoundsExample()
}
NavigationLink("LineStyleLayerExample") {
LineStyleLayerExampleUIViewControllerRepresentable()
}
Expand Down
28 changes: 28 additions & 0 deletions platform/ios/app-swift/Sources/MaximumScreenBoundsExample.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import MapLibre
import SwiftUI
import UIKit

// Denver, Colorado
private let center = CLLocationCoordinate2D(latitude: 39.748947, longitude: -104.995882)

// Colorado’s bounds
private let colorado = MLNCoordinateBounds(
sw: CLLocationCoordinate2D(latitude: 36.986207, longitude: -109.049896),
ne: CLLocationCoordinate2D(latitude: 40.989329, longitude: -102.062592)
)

struct MaximumScreenBoundsExample: UIViewRepresentable {
func makeUIView(context _: Context) -> MLNMapView {
let mapView = MLNMapView(frame: .zero, styleURL: VERSATILES_COLORFUL_STYLE)
mapView.setCenter(center, zoomLevel: 10, direction: 0, animated: false)
mapView.maximumScreenBounds = MLNCoordinateBounds(sw: colorado.sw, ne: colorado.ne)

return mapView
}

func updateUIView(_: MLNMapView, context _: Context) {}

func makeCoordinator() -> Coordinator {
Coordinator()
}
}
7 changes: 7 additions & 0 deletions platform/ios/src/MLNMapView.h
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,13 @@ vertically on the map.
*/
@property (nonatomic) double maximumZoomLevel;

/**
* The maximum bounds of the map that can be shown on screen.
*
* @param MLNCoordinateBounds the bounds to constrain the screen to.
*/
@property (nonatomic) MLNCoordinateBounds maximumScreenBounds;

/**
The heading of the map, measured in degrees clockwise from true north.

Expand Down
15 changes: 15 additions & 0 deletions platform/ios/src/MLNMapView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -3867,6 +3867,21 @@ - (double)maximumZoomLevel
return *self.mbglMap.getBounds().maxZoom;
}

- (void)setMaximumScreenBounds:(MLNCoordinateBounds)maximumScreenBounds
{
mbgl::LatLng sw = {maximumScreenBounds.sw.latitude, maximumScreenBounds.sw.longitude};
mbgl::LatLng ne = {maximumScreenBounds.ne.latitude, maximumScreenBounds.ne.longitude};
mbgl::BoundOptions newBounds = mbgl::BoundOptions().withLatLngBounds(mbgl::LatLngBounds::hull(sw, ne));

self.mbglMap.setBounds(newBounds);
self.mbglMap.setConstrainMode(mbgl::ConstrainMode::Screen);
}

- (MLNCoordinateBounds)maximumScreenBounds
{
return MLNCoordinateBoundsFromLatLngBounds(*self.mbglMap.getBounds().bounds);;
}

- (CGFloat)minimumPitch
{
return *self.mbglMap.getBounds().minPitch;
Expand Down
38 changes: 34 additions & 4 deletions src/mbgl/map/transform.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,23 @@ void Transform::resize(const Size size) {
double scale{state.getScale()};
double x{state.getX()};
double y{state.getY()};

double lat;
double lon;
if (state.constrainScreen(scale, lat, lon)) {
// Turns out that if you resize during a transition any changes made to the state will be ignored :(
// So if we have to constrain because of a resize and a transition is in progress - cancel the transition!
if (inTransition()) {
cancelTransitions();
}

// It also turns out that state.setProperties isn't enough if you change the center, you need to set Cc and Bc
// too, which setLatLngZoom does.
state.setLatLngZoom(mbgl::LatLng{lat, lon}, state.scaleZoom(scale));
observer.onCameraDidChange(MapObserver::CameraChangeMode::Immediate);

return;
}
state.constrain(scale, x, y);
state.setProperties(TransformStateProperties().withScale(scale).withX(x).withY(y));

Expand All @@ -86,17 +103,23 @@ void Transform::jumpTo(const CameraOptions& camera) {
* smooth animation between old and new values. The map will retain the current
* values for any options not included in `options`.
*/
void Transform::easeTo(const CameraOptions& camera, const AnimationOptions& animation) {
void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& animation) {
CameraOptions camera = inputCamera;

Duration duration = animation.duration.value_or(Duration::zero());
if (state.getLatLngBounds() == LatLngBounds() && !isGestureInProgress() && duration != Duration::zero()) {
// reuse flyTo, without exaggerated animation, to achieve constant ground speed.
return flyTo(camera, animation, true);
}

double zoom = camera.zoom.value_or(getZoom());
state.constrainCameraAndZoomToBounds(camera, zoom);

const EdgeInsets& padding = camera.padding.value_or(state.getEdgeInsets());
LatLng startLatLng = getLatLng(LatLng::Unwrapped);
const LatLng& unwrappedLatLng = camera.center.value_or(startLatLng);
const LatLng& latLng = state.getLatLngBounds() != LatLngBounds() ? unwrappedLatLng : unwrappedLatLng.wrapped();
double zoom = camera.zoom.value_or(getZoom());

double bearing = camera.bearing ? util::deg2rad(-*camera.bearing) : getBearing();
double pitch = camera.pitch ? util::deg2rad(*camera.pitch) : getPitch();

Expand Down Expand Up @@ -176,10 +199,17 @@ void Transform::easeTo(const CameraOptions& camera, const AnimationOptions& anim

Where applicable, local variable documentation begins with the associated
variable or function in van Wijk (2003). */
void Transform::flyTo(const CameraOptions& camera, const AnimationOptions& animation, bool linearZoomInterpolation) {
void Transform::flyTo(const CameraOptions& inputCamera,
const AnimationOptions& animation,
bool linearZoomInterpolation) {
CameraOptions camera = inputCamera;

double zoom = camera.zoom.value_or(getZoom());
state.constrainCameraAndZoomToBounds(camera, zoom);

const EdgeInsets& padding = camera.padding.value_or(state.getEdgeInsets());
const LatLng& latLng = camera.center.value_or(getLatLng(LatLng::Unwrapped)).wrapped();
double zoom = camera.zoom.value_or(getZoom());

double bearing = camera.bearing ? util::deg2rad(-*camera.bearing) : getBearing();
double pitch = camera.pitch ? util::deg2rad(*camera.pitch) : getPitch();

Expand Down
146 changes: 145 additions & 1 deletion src/mbgl/map/transform_state.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -748,8 +748,27 @@ bool TransformState::rotatedNorth() const {
return (orientation == NO::Leftwards || orientation == NO::Rightwards);
}

bool TransformState::constrainScreen(double& scale_, double& lat, double& lon) const {
if (constrainMode == ConstrainMode::Screen) {
double zoom = scaleZoom(scale_);
CameraOptions options = CameraOptions();
constrainCameraAndZoomToBounds(options, zoom);

scale_ = zoomScale(zoom);

if (options.center) {
LatLng center = options.center.value();
lat = center.latitude();
lon = center.longitude();

return true;
}
}
return false;
}

void TransformState::constrain(double& scale_, double& x_, double& y_) const {
if (constrainMode == ConstrainMode::None) {
if (constrainMode == ConstrainMode::None || constrainMode == ConstrainMode::Screen) {
return;
}

Expand All @@ -768,6 +787,131 @@ void TransformState::constrain(double& scale_, double& x_, double& y_) const {
}
}

void TransformState::constrainCameraAndZoomToBounds(CameraOptions& requestedCamera, double& requestedZoom) const {
if (constrainMode != ConstrainMode::Screen || getLatLngBounds() == LatLngBounds()) {
return;
}

LatLng centerLatLng = getLatLng();

if (requestedCamera.center) {
centerLatLng = requestedCamera.center.value();
}

Point<double> anchorOffset{0, 0};
double requestedScale = zoomScale(requestedZoom);

// Since the transition calculations will include any specified anchor in the result
// we need to do the same when testing if the requested center and zoom is outside the bounds or not.
if (requestedCamera.anchor) {
ScreenCoordinate anchor = requestedCamera.anchor.value();
anchor.y = getSize().height - anchor.y;
LatLng anchorLatLng = screenCoordinateToLatLng(anchor);

// The screenCoordinateToLatLng function requires the matrices inside the state to reflect
// the requested scale. So we create a copy and set the requested zoom before the conversion.
// This will give us the same result as the transition calculations.
TransformState state{*this};
state.setLatLngZoom(getLatLng(), scaleZoom(requestedScale));
LatLng screenLatLng = state.screenCoordinateToLatLng(anchor);

auto latLngCoord = Projection::project(anchorLatLng, requestedScale);
auto anchorCoord = Projection::project(screenLatLng, requestedScale);
anchorOffset = latLngCoord - anchorCoord;
}

mbgl::LatLngBounds currentBounds = getLatLngBounds();
mbgl::ScreenCoordinate neBounds = Projection::project(currentBounds.northeast(), requestedScale);
mbgl::ScreenCoordinate swBounds = Projection::project(currentBounds.southwest(), requestedScale);
mbgl::ScreenCoordinate center = Projection::project(centerLatLng, requestedScale);
mbgl::ScreenCoordinate currentCenter = Projection::project(getLatLng(), requestedScale);

double minY = neBounds.y;
double maxY = swBounds.y;
double minX = swBounds.x;
double maxX = neBounds.x;

double startX = center.x;
double startY = center.y;

double resultX = startX;
double resultY = startY;

uint32_t screenWidth = getSize().width;
uint32_t screenHeight = getSize().height;

double h2 = screenHeight / 2.0;
if (startY - h2 + anchorOffset.y < minY) {
resultY = minY + h2;
}
if (startY + anchorOffset.y + h2 > maxY) {
resultY = maxY - h2;
}

double w2 = screenWidth / 2.0;
if (startX + anchorOffset.x - w2 < minX) {
resultX = minX + w2;
}
if (startX + anchorOffset.x + w2 > maxX) {
resultX = maxX - w2;
}

double scaleY = 0;
if (maxY - minY < screenHeight) {
scaleY = screenHeight / (maxY - minY);
resultY = (maxY + minY) / 2.0;
}

double scaleX = 0;
if (maxX - minX < screenWidth) {
scaleX = screenWidth / (maxX - minX);
resultX = (maxX + minX) / 2.0;
}

double maxScale = scaleX > scaleY ? scaleX : scaleY;

// Max scale will be 1 when the screen is exactly the same size as the max bounds in either the X or Y direction.
// To avoid numerical instabilities we add small amount to the check to make sure we don't try to scale when we
// don't actually need it.
if (maxScale > 1.000001) {
requestedZoom += scaleZoom(maxScale);

if (scaleY > scaleX) {
// If we scaled the y direction we want the resulting x position to be the same as the current x position.
resultX = currentCenter.x;
} else {
// If we scaled the x direction we want the resulting y position to be the same as the current y position.
resultY = currentCenter.y;
}

// Since we changed the scale, we might display something outside the bounds.
// When checking we need to take into consideration that we just changed the scale,
// since the resultX and minX were calculated with the requested scale, and not the scale we
// just calculated to make sure we stay inside the bounds.
if (resultX * maxScale - w2 <= minX * maxScale) {
resultX = minX * maxScale + w2;
resultX /= maxScale;
} else if (resultX * maxScale + w2 >= maxX * maxScale) {
resultX = maxX * maxScale - w2;
resultX /= maxScale;
}

if (resultY * maxScale - h2 <= minY * maxScale) {
resultY = minY * maxScale + h2;
resultY /= maxScale;
} else if (resultY * maxScale + h2 >= maxY * maxScale) {
resultY = maxY * maxScale - h2;
resultY /= maxScale;
}
}

if (resultX != startX || resultY != startY) {
// If we made changes just drop any anchor point
requestedCamera.anchor.reset();
requestedCamera.center = std::optional(Projection::unproject({resultX, resultY}, requestedScale));
}
}

ScreenCoordinate TransformState::getCenterOffset() const {
return {0.5 * (edgeInsets.left() - edgeInsets.right()), 0.5 * (edgeInsets.top() - edgeInsets.bottom())};
}
Expand Down
3 changes: 3 additions & 0 deletions src/mbgl/map/transform_state.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ class TransformState {
void setLatLngZoom(const LatLng& latLng, double zoom);

void constrain(double& scale, double& x, double& y) const;
bool constrainScreen(double& scale_, double& x_, double& y_) const;
void constrainCameraAndZoomToBounds(CameraOptions& camera, double& zoom) const;

const mat4& getProjectionMatrix() const;
const mat4& getInvProjectionMatrix() const;

Expand Down
Loading
Loading