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

Bugfix: _cameraForBoxAndBearing not fitting bounds properly when using asymettrical camera viewport and bearing #3591

Merged
merged 7 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

### 🐞 Bug fixes
- Fix wheel zoom to be into the same direction above or under the horizon ([#3398](https://github.com/maplibre/maplibre-gl-js/issues/3398))
- Fix _cameraForBoxAndBearing not fitting bounds properly when using asymettrical camera viewport and bearing.
- _...Add new stuff here..._

## 4.0.0-pre.4
Expand Down
23 changes: 21 additions & 2 deletions src/ui/camera.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {mercatorZfromAltitude} from '../geo/mercator_coordinate';
import {Terrain} from '../render/terrain';
import {LngLat, LngLatLike} from '../geo/lng_lat';
import {Event} from '../util/evented';
import {LngLatBounds} from '../geo/lng_lat_bounds';

beforeEach(() => {
setMatchMedia();
Expand Down Expand Up @@ -1930,7 +1931,7 @@ describe('#cameraForBounds', () => {
const transform = camera.cameraForBounds(bb, {bearing: 175});

expect(fixedLngLat(transform.center, 4)).toEqual({lng: -100.5, lat: 34.7171});
expect(fixedNum(transform.zoom, 3)).toBe(2.558);
expect(fixedNum(transform.zoom, 3)).toBe(2.396);
expect(transform.bearing).toBe(175);
});

Expand All @@ -1940,7 +1941,7 @@ describe('#cameraForBounds', () => {
const transform = camera.cameraForBounds(bb, {bearing: -30});

expect(fixedLngLat(transform.center, 4)).toEqual({lng: -100.5, lat: 34.7171});
expect(fixedNum(transform.zoom, 3)).toBe(2.392);
expect(fixedNum(transform.zoom, 3)).toBe(2.222);
expect(transform.bearing).toBe(-30);
});

Expand Down Expand Up @@ -2000,6 +2001,24 @@ describe('#cameraForBounds', () => {

expect(fixedLngLat(transform.center, 4)).toEqual({lng: -103.3761, lat: 43.0929});
});

test('asymmetrical transform using LngLatBounds instance', () => {
const transform = new Transform(2, 10, 0, 60, false);
transform.resize(2048, 512);

const camera = attachSimulateFrame(new CameraMock(transform, {} as any));
camera._update = () => {};

const bb = new LngLatBounds();
bb.extend([-66.9326, 49.5904]);
bb.extend([-125.0011, 24.9493]);

const rotatedTransform = camera.cameraForBounds(bb, {bearing: 45});

expect(fixedLngLat(rotatedTransform.center, 4)).toEqual({lng: -95.9669, lat: 38.3048});
expect(fixedNum(rotatedTransform.zoom, 3)).toBe(2.507);
expect(rotatedTransform.bearing).toBe(45);
});
});

describe('#fitBounds', () => {
Expand Down
40 changes: 29 additions & 11 deletions src/ui/camera.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {extend, warnOnce, clamp, wrap, defaultEasing, pick} from '../util/util';
import {extend, warnOnce, clamp, wrap, defaultEasing, pick, degreesToRadians} from '../util/util';
import {interpolates} from '@maplibre/maplibre-gl-style-spec';
import {browser} from '../util/browser';
import {LngLat} from '../geo/lng_lat';
Expand Down Expand Up @@ -692,15 +692,30 @@ export abstract class Camera extends Evented {
const tr = this.transform;
const edgePadding = tr.padding;

// We want to calculate the upper right and lower left of the box defined by p0 and p1
// in a coordinate system rotate to match the destination bearing.
const p0world = tr.project(LngLat.convert(p0));
const p1world = tr.project(LngLat.convert(p1));
const p0rotated = p0world.rotate(-bearing * Math.PI / 180);
const p1rotated = p1world.rotate(-bearing * Math.PI / 180);
// Consider all corners of the rotated bounding box derived from the given points
// when find the camera position that fits the given points.
const bounds = new LngLatBounds(p0, p1);
const nwWorld = tr.project(bounds.getNorthWest());
const neWorld = tr.project(bounds.getNorthEast());
const seWorld = tr.project(bounds.getSouthEast());
const swWorld = tr.project(bounds.getSouthWest());

const upperRight = new Point(Math.max(p0rotated.x, p1rotated.x), Math.max(p0rotated.y, p1rotated.y));
const lowerLeft = new Point(Math.min(p0rotated.x, p1rotated.x), Math.min(p0rotated.y, p1rotated.y));
const bearingRadians = degreesToRadians(-bearing);

const nwRotatedWorld = nwWorld.rotate(bearingRadians);
const neRotatedWorld = neWorld.rotate(bearingRadians);
const seRotatedWorld = seWorld.rotate(bearingRadians);
const swRotatedWorld = swWorld.rotate(bearingRadians);

const upperRight = new Point(
Math.max(nwRotatedWorld.x, neRotatedWorld.x, swRotatedWorld.x, seRotatedWorld.x),
Math.max(nwRotatedWorld.y, neRotatedWorld.y, swRotatedWorld.y, seRotatedWorld.y)
);

const lowerLeft = new Point(
Math.min(nwRotatedWorld.x, neRotatedWorld.x, swRotatedWorld.x, seRotatedWorld.x),
Math.min(nwRotatedWorld.y, neRotatedWorld.y, swRotatedWorld.y, seRotatedWorld.y)
);

// Calculate zoom: consider the original bbox and padding.
const size = upperRight.sub(lowerLeft);
Expand All @@ -721,11 +736,14 @@ export abstract class Camera extends Evented {
const paddingOffsetX = (options.padding.left - options.padding.right) / 2;
const paddingOffsetY = (options.padding.top - options.padding.bottom) / 2;
const paddingOffset = new Point(paddingOffsetX, paddingOffsetY);
const rotatedPaddingOffset = paddingOffset.rotate(bearing * Math.PI / 180);
const rotatedPaddingOffset = paddingOffset.rotate(degreesToRadians(bearing));
const offsetAtInitialZoom = offset.add(rotatedPaddingOffset);
const offsetAtFinalZoom = offsetAtInitialZoom.mult(tr.scale / tr.zoomScale(zoom));

const center = tr.unproject(p0world.add(p1world).div(2).sub(offsetAtFinalZoom));
const center = tr.unproject(
// either world diagonal can be used (NW-SE or NE-SW)
nwWorld.add(seWorld).div(2).sub(offsetAtFinalZoom)
);

return {
center,
Expand Down
10 changes: 10 additions & 0 deletions src/util/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -679,3 +679,13 @@ export function subscribe(target: Subscriber, message: keyof WindowEventMap, lis
}
};
}

/**
* This method converts degrees to radians.
* The return value is the radian value.
* @param degrees - The number of degrees
* @returns radians
*/
export function degreesToRadians(degrees: number): number {
return degrees * Math.PI / 180;
}