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

Fix to map jumping when bounds cross longitude 180 #10903

Merged
merged 8 commits into from
Aug 13, 2021
Merged
41 changes: 41 additions & 0 deletions debug/antimeridian.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html>
<head>
<title>Mapbox GL JS debug page</title>
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel='stylesheet' href='../dist/mapbox-gl.css' />
<style>
body { margin: 0; padding: 0; }
html, body, #map { height: 100%; }
</style>
</head>

<body>
<div id='map'></div>

<script src='../dist/mapbox-gl-dev.js'></script>
<script src='../debug/access_token_generated.js'></script>

<script>

var map = window.map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v11',
zoom: 6,
center: [179.012, -18.124],
maxBounds: [[175, -25], [-175, -10]]
});
map.transform.center = new mapboxgl.LngLat(190, -40);

document.addEventListener('keypress', (e) => {
console.log(e.key);
if (e.key === " ") {
console.log('map.transform.center:', map.transform.center);
console.log('map.transform.point:', map.transform.point);
}
});

</script>
</body>
</html>
39 changes: 26 additions & 13 deletions src/geo/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -887,17 +887,20 @@ class Transform {
zoomScale(zoom: number) { return Math.pow(2, zoom); }
scaleZoom(scale: number) { return Math.log(scale) / Math.LN2; }

// Transform from LngLat to Point in world coordinates [-180, 180] x [90, -90] --> [0, this.worldSize] x [0, this.worldSize]
project(lnglat: LngLat) {
const lat = clamp(lnglat.lat, -this.maxValidLatitude, this.maxValidLatitude);
return new Point(
mercatorXfromLng(lnglat.lng) * this.worldSize,
mercatorYfromLat(lat) * this.worldSize);
}

// Transform from Point in world coordinates to LngLat [0, this.worldSize] x [0, this.worldSize] --> [-180, 180] x [90, -90]
unproject(point: Point): LngLat {
return new MercatorCoordinate(point.x / this.worldSize, point.y / this.worldSize).toLngLat();
}

// Point at center in world coordinates.
get point(): Point { return this.project(this.center); }

setLocationAtPoint(lnglat: LngLat, point: Point) {
Expand Down Expand Up @@ -1210,7 +1213,10 @@ class Transform {
*/
setMaxBounds(bounds?: LngLatBounds) {
if (bounds) {
this.lngRange = [bounds.getWest(), bounds.getEast()];
const eastBound = bounds.getEast();
const westBound = bounds.getWest();
// Unwrap bounds if they cross the 180th meridian
this.lngRange = [westBound, eastBound > westBound ? eastBound : eastBound + 360];
this.latRange = [bounds.getSouth(), bounds.getNorth()];
this._constrain();
} else {
Expand Down Expand Up @@ -1355,11 +1361,9 @@ class Transform {

this._constraining = true;

let minY = -90;
let maxY = 90;
let minX = -180;
let maxX = 180;
let sy, sx, x2, y2;
let minY = Infinity;
SnailBones marked this conversation as resolved.
Show resolved Hide resolved
let maxY = -Infinity;
SnailBones marked this conversation as resolved.
Show resolved Hide resolved
let minX, maxX, sy, sx, y2;
SnailBones marked this conversation as resolved.
Show resolved Hide resolved
const size = this.size,
unmodified = this._unmodified;

Expand Down Expand Up @@ -1400,18 +1404,27 @@ class Transform {
if (y + h2 > maxY) y2 = maxY - h2;
}

if (this.lngRange) {
const x = point.x,
w2 = size.x / 2;
let x = point.x;

if (x - w2 < minX) x2 = minX + w2;
if (x + w2 > maxX) x2 = maxX - w2;
if (this.lngRange) {
// Translate to positive positions with the map center in the center position.
// This ensures that the map snaps to the correct edge.
const shift = this.worldSize / 2 - (minX + maxX) / 2;
x = (x + shift + this.worldSize) % this.worldSize;
minX += shift;
maxX += shift;

const w2 = size.x / 2;
if (x - w2 < minX) x = minX + w2;
if (x + w2 > maxX) x = maxX - w2;

x -= shift;
}

// pan the map if the screen goes off the range
if (x2 !== undefined || y2 !== undefined) {
if (x !== point.x || y2 !== undefined) {
this.center = this.unproject(new Point(
x2 !== undefined ? x2 : point.x,
x,
y2 !== undefined ? y2 : point.y));
}

Expand Down
106 changes: 106 additions & 0 deletions test/unit/geo/transform.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,112 @@ test('transform', (t) => {
t.end();
});

t.test('maxBounds should not jump to the wrong side when crossing 180th meridian (#10447)', (t) => {
t.test(' to the East', (t) => {
const transform = new Transform();
transform.zoom = 6;
transform.resize(500, 500);
transform.lngRange = [160, 190];
transform.latRange = [-55, -23];

transform.center = new LngLat(-170, -40);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does -170 come from? I'd expect (160 + 190) / 2 --> 175. -170 is equivalent to 190, so is it using the east bound as the center?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this test sets the center to the east bound in order to test that the map position snaps to the correct side. The exact position is arbitrary, it could be any position out of bounds on the east side of the map.


t.ok(transform.center.lng < 190);
t.ok(transform.center.lng > 175);

t.end();
});

t.test('to the West', (t) => {
const transform = new Transform();
transform.zoom = 6;
transform.resize(500, 500);
transform.lngRange = [-190, -160];
transform.latRange = [-55, -23];

transform.center = new LngLat(170, -40);

t.ok(transform.center.lng > -190);
t.ok(transform.center.lng < -175);

t.end();
});

t.test('longitude 0 - 360', (t) => {
const transform = new Transform();
transform.zoom = 6;
transform.resize(500, 500);
transform.lngRange = [0, 360];
transform.latRange = [-90, 90];

transform.center = new LngLat(-155, 0);

t.same(transform.center, new LngLat(205, 0));

t.end();
});

t.test('longitude -360 - 0', (t) => {
const transform = new Transform();
transform.zoom = 6;
transform.resize(500, 500);
transform.lngRange = [-360, 0];
transform.latRange = [-90, 90];

transform.center = new LngLat(160, 0);
t.same(transform.center.lng.toFixed(10), -200);

t.end();
});

t.end();

});

t.test('maxBounds snaps in the correct direction (no forcing to other edge when width < 360)', (t) => {
const transform = new Transform();
transform.zoom = 6;
transform.resize(500, 500);
transform.setMaxBounds(new LngLatBounds([-160, -20], [160, 20]));

transform.center = new LngLat(170, 0);
t.ok(transform.center.lng > 150);
t.ok(transform.center.lng < 160);

transform.center = new LngLat(-170, 0);
t.ok(transform.center.lng > -160);
t.ok(transform.center.lng < -150);

t.end();
});

t.test('maxBounds works with unwrapped values across the 180th meridian (#6985)', (t) => {
const transform = new Transform();
transform.zoom = 6;
transform.resize(500, 500);
transform.setMaxBounds(new LngLatBounds([160, -20], [-160, 20])); //East bound is "smaller"

const wrap = val => ((val + 360) % 360);

transform.center = new LngLat(170, 0);
t.same(wrap(transform.center.lng), 170);

transform.center = new LngLat(-170, 0);
t.same(wrap(transform.center.lng), wrap(-170));

transform.center = new LngLat(150, 0);
let lng = wrap(transform.center.lng);
t.ok(lng > 160);
t.ok(lng < 180);

transform.center = new LngLat(-150, 0);
lng = wrap(transform.center.lng);
t.ok(lng < 360 - 160);
t.ok(lng > 360 - 180);

t.end();
});

t.test('_minZoomForBounds respects latRange and lngRange', (t) => {
t.test('it returns 0 when latRange and lngRange are undefined', (t) => {
const transform = new Transform();
Expand Down