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
@@ -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 = new mapboxgl.Map({
SnailBones marked this conversation as resolved.
Show resolved Hide resolved
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);
SnailBones marked this conversation as resolved.
Show resolved Hide resolved
console.log(map.transform.point);
SnailBones marked this conversation as resolved.
Show resolved Hide resolved
}
});

</script>
</body>
</html>
39 changes: 26 additions & 13 deletions src/geo/transform.js
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; }

// World coordinates from LngLat
Copy link
Contributor

Choose a reason for hiding this comment

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

Just an idea: It doesn't transfer directly, but when I added comments to explain numerical units, I added ranges since that's the easiest way for me to immediately understand the meaning:

// Transform from screen coordinates to GL NDC, [0, w] x [h, 0] --> [-1, 1] x [-1, 1]
// Roughly speaking, applies pixelsToGLUnits scaling with a translation
glCoordMatrix: Float32Array;
// Inverse of glCoordMatrix, from NDC to screen coordinates, [-1, 1] x [-1, 1] --> [0, w] x [h, 0]
labelPlaneMatrix: Float32Array;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great idea!

Copy link
Contributor Author

@SnailBones SnailBones Aug 9, 2021

Choose a reason for hiding this comment

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

How's this?

Suggested change
// World coordinates from LngLat
// Transform from LngLat to 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);
}

// LngLat from world coordinates
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 = 0;
SnailBones marked this conversation as resolved.
Show resolved Hide resolved
let maxY = 0;
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
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