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();