diff --git a/CHANGELOG.md b/CHANGELOG.md index 5382674a4c..8c6ee25994 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ ## main ### ✨ Features and improvements -- _...Add new stuff here..._ - Add option to position popup at subpixel coordinates to allow for smooth animations ([#3710](https://github.com/maplibre/maplibre-gl-js/pull/3710)) +- Constrain horizontal panning when renderWorldCopies is set to false ([3738](https://github.com/maplibre/maplibre-gl-js/pull/3738)) +- _...Add new stuff here..._ ### 🐞 Bug fixes - Fix popup appearing far from marker that was moved to a side globe ([3712](https://github.com/maplibre/maplibre-gl-js/pull/3712)) diff --git a/src/geo/transform.ts b/src/geo/transform.ts index 996a2140cf..cfa1120cd5 100644 --- a/src/geo/transform.ts +++ b/src/geo/transform.ts @@ -714,6 +714,13 @@ export class Transform { _constrain() { if (!this.center || !this.width || !this.height || this._constraining) return; + let lngRange = this.lngRange; + + if (!this._renderWorldCopies && lngRange === null) { + const almost180 = 180 - 1e-10; + lngRange = [-almost180, almost180]; + } + this._constraining = true; let minY = -90; @@ -731,9 +738,7 @@ export class Transform { sy = maxY - minY < size.y ? size.y / (maxY - minY) : 0; } - if (this.lngRange) { - const lngRange = this.lngRange; - + if (lngRange) { minX = wrap( mercatorXfromLng(lngRange[0]) * this.worldSize, 0, @@ -773,9 +778,12 @@ export class Transform { if (y + h2 > maxY) y2 = maxY - h2; } - if (this.lngRange) { + if (lngRange) { const centerX = (minX + maxX) / 2; - const x = wrap(point.x, centerX - this.worldSize / 2, centerX + this.worldSize / 2); + let x = point.x; + if (this._renderWorldCopies) { + x = wrap(point.x, centerX - this.worldSize / 2, centerX + this.worldSize / 2); + } const w2 = size.x / 2; if (x - w2 < minX) x2 = minX + w2; diff --git a/src/ui/map.test.ts b/src/ui/map.test.ts index 9fdb8cd692..25c4a8903a 100755 --- a/src/ui/map.test.ts +++ b/src/ui/map.test.ts @@ -1079,6 +1079,63 @@ describe('Map', () => { }); + describe('renderWorldCopies', () => { + test('does not constrain horizontal panning when renderWorldCopies is set to true', () => { + const map = createMap({renderWorldCopies: true}); + map.setCenter({lng: 180, lat: 0}); + expect(map.getCenter().lng).toBe(180); + }); + + test('constrains horizontal panning when renderWorldCopies is set to false', () => { + const map = createMap({renderWorldCopies: false}); + map.setCenter({lng: 180, lat: 0}); + expect(map.getCenter().lng).toBeCloseTo(110, 0); + }); + + test('does not wrap the map when renderWorldCopies is set to false', () => { + const map = createMap({renderWorldCopies: false}); + map.setCenter({lng: 200, lat: 0}); + expect(map.getCenter().lng).toBeCloseTo(110, 0); + }); + + test('panTo is constrained to single globe when renderWorldCopies is set to false', () => { + const map = createMap({renderWorldCopies: false}); + map.panTo({lng: 180, lat: 0}, {duration: 0}); + expect(map.getCenter().lng).toBeCloseTo(110, 0); + map.panTo({lng: -3000, lat: 0}, {duration: 0}); + expect(map.getCenter().lng).toBeCloseTo(-110, 0); + }); + + test('flyTo is constrained to single globe when renderWorldCopies is set to false', () => { + const map = createMap({renderWorldCopies: false}); + map.flyTo({center: [1000, 0], zoom: 3, animate: false}); + expect(map.getCenter().lng).toBeCloseTo(171, 0); + map.flyTo({center: [-1000, 0], zoom: 5, animate: false}); + expect(map.getCenter().lng).toBeCloseTo(-178, 0); + }); + + test('lng is constrained to a single globe when zooming with {renderWorldCopies: false}', () => { + const map = createMap({renderWorldCopies: false, center: [180, 0], zoom: 2}); + expect(map.getCenter().lng).toBeCloseTo(162, 0); + map.zoomTo(1, {animate: false}); + expect(map.getCenter().lng).toBeCloseTo(145, 0); + }); + + test('lng is constrained by maxBounds when {renderWorldCopies: false}', () => { + const map = createMap({ + renderWorldCopies: false, + maxBounds: [ + [70, 30], + [80, 40] + ], + zoom: 8, + center: [75, 35] + }); + map.setCenter({lng: 180, lat: 0}); + expect(map.getCenter().lng).toBeCloseTo(80, 0); + }); + }); + test('#setMinZoom', () => { const map = createMap({zoom: 5}); map.setMinZoom(3.5); diff --git a/src/ui/marker.test.ts b/src/ui/marker.test.ts index 69165b513a..a061ea465e 100644 --- a/src/ui/marker.test.ts +++ b/src/ui/marker.test.ts @@ -8,6 +8,7 @@ import type {Terrain} from '../render/terrain'; type MapOptions = { width?: number; + renderWorldCopies?: boolean; } function createMap(options: MapOptions = {}) { @@ -1027,4 +1028,15 @@ describe('marker', () => { expect(marker.getElement().style.opacity).toMatch('0.35'); map.remove(); }); + + test('Marker\'s lng is wrapped when slightly crossing 180 with {renderWorldCopies: false}', () => { + const map = createMap({width: 1024, renderWorldCopies: false}); + const marker = new Marker() + .setLngLat([179, 0]) + .addTo(map); + + marker.setLngLat([181, 0]); + + expect(marker._lngLat.lng).toBe(-179); + }); }); diff --git a/src/ui/marker.ts b/src/ui/marker.ts index 35fe3143c1..30b7690ff2 100644 --- a/src/ui/marker.ts +++ b/src/ui/marker.ts @@ -572,6 +572,8 @@ export class Marker extends Evented { if (this._map.transform.renderWorldCopies) { this._lngLat = smartWrap(this._lngLat, this._flatPos, this._map.transform); + } else { + this._lngLat = this._lngLat?.wrap(); } this._flatPos = this._pos = this._map.project(this._lngLat)._add(this._offset); diff --git a/src/ui/popup.ts b/src/ui/popup.ts index ff150489d1..97bf0d48c3 100644 --- a/src/ui/popup.ts +++ b/src/ui/popup.ts @@ -596,6 +596,8 @@ export class Popup extends Evented { if (this._map.transform.renderWorldCopies && !this._trackPointer) { this._lngLat = smartWrap(this._lngLat, this._flatPos, this._map.transform); + } else { + this._lngLat = this._lngLat?.wrap(); } if (this._trackPointer && !cursor) return; diff --git a/test/examples/render-world-copies.html b/test/examples/render-world-copies.html index abf36446a9..3e4b54a197 100644 --- a/test/examples/render-world-copies.html +++ b/test/examples/render-world-copies.html @@ -52,6 +52,7 @@ function switchRenderOption(option) { const status = option.target.id; map.setRenderWorldCopies(status === 'true'); + map.panTo(map.getCenter()); } for (let i = 0; i < inputs.length; i++) {