diff --git a/debug/antimeridian.html b/debug/antimeridian.html
new file mode 100644
index 00000000000..c0f8bfb4338
--- /dev/null
+++ b/debug/antimeridian.html
@@ -0,0 +1,41 @@
+
+
+
+ Mapbox GL JS debug page
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/geo/transform.js b/src/geo/transform.js
index 692ea9760cb..d2b71fcb35f 100644
--- a/src/geo/transform.js
+++ b/src/geo/transform.js
@@ -887,6 +887,7 @@ 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(
@@ -894,10 +895,12 @@ class Transform {
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) {
@@ -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 {
@@ -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;
+ let maxY = -Infinity;
+ let minX, maxX, sy, sx, y2;
const size = this.size,
unmodified = this._unmodified;
@@ -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));
}
diff --git a/test/unit/geo/transform.test.js b/test/unit/geo/transform.test.js
index a5abf28c2e0..0744b3278f1 100644
--- a/test/unit/geo/transform.test.js
+++ b/test/unit/geo/transform.test.js
@@ -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);
+
+ 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();