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 6 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
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("MaximumBoundsExample") {
MaximumBoundsExample()
}
}
}
}
Expand Down
29 changes: 29 additions & 0 deletions platform/ios/app-swift/Sources/MaximumBoundsExample.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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 MaximumBoundsExample: 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.delegate = context.coordinator
mapView.maximumBounds = 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 @@ -874,6 +874,13 @@ MLN_EXPORT
*/
@property (nonatomic) double maximumZoomLevel;

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

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

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

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

self.mbglMap.setBounds(newBounds);
}

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

- (CGFloat)minimumPitch
{
return *self.mbglMap.getBounds().minPitch;
Expand Down
21 changes: 17 additions & 4 deletions src/mbgl/map/transform.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,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 @@ -174,10 +180,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
120 changes: 120 additions & 0 deletions src/mbgl/map/transform_state.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,126 @@ void TransformState::constrain(double& scale_, double& x_, double& y_) const {
}
}

void TransformState::constrainCameraAndZoomToBounds(CameraOptions& camera, double& zoom) const {
if (getLatLngBounds() == LatLngBounds()) {
return;
}

LatLng centerLatLng = getLatLng();

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

Point<double> anchorOffset{0, 0};
double currentScale = getScale();
double requestedScale = zoomScale(zoom);

// 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 (camera.anchor) {
ScreenCoordinate anchor = camera.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 bounds = getLatLngBounds();
mbgl::ScreenCoordinate neBounds = Projection::project(bounds.northeast(), requestedScale);
mbgl::ScreenCoordinate swBounds = Projection::project(bounds.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;
if (maxScale > 1 && camera.anchor) {
zoom += scaleZoom(maxScale);

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

// 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;
}
} else {
resultY = currentCenter.y;
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 any changes just drop any anchor point
camera.anchor.reset();
camera.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
2 changes: 2 additions & 0 deletions src/mbgl/map/transform_state.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ class TransformState {
void setLatLngZoom(const LatLng& latLng, double zoom);

void constrain(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