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

Keep camera outside terrain #4300

Merged
merged 28 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
731d514
Don't reuse old requestedCameraState.
chrneumann Jun 6, 2024
58d908d
Delete requestedCameraState after it is applied to the transform.
chrneumann Jun 13, 2024
4266d81
Add test for camera inside terrain fix.
chrneumann Jun 19, 2024
49fa7dc
Move camera higher and fix pitch when about to enter terrain.
chrneumann Jun 19, 2024
564fb90
Revert "Delete requestedCameraState after it is applied to the transf…
chrneumann Jul 23, 2024
c096dfc
Revert "Don't reuse old requestedCameraState."
chrneumann Jul 23, 2024
00c9e31
Merge branch 'main' into fix/keep-camera-outside-terrain
chrneumann Jul 23, 2024
08fa9ee
Move camera elevation fix to Camera class.
chrneumann Jul 23, 2024
f6bdf15
Fix direct manipulation of transform.
chrneumann Jul 23, 2024
7508b56
Cleanup code.
chrneumann Jul 23, 2024
42ec303
Fix tests.
chrneumann Jul 23, 2024
799f6c4
Update src/ui/camera.ts
HarelM Jul 24, 2024
f2938ee
Merge branch 'main' into fix/keep-camera-outside-terrain
HarelM Jul 24, 2024
818f4ce
Refactor Camera::_applyUpdateTransform method.
chrneumann Jul 30, 2024
ae67766
Fix render tests for terrain.
chrneumann Jul 30, 2024
513726b
Update src/ui/map.ts
HarelM Jul 30, 2024
ea6c2ef
Merge branch 'main' into fix/keep-camera-outside-terrain
HarelM Jul 30, 2024
496ef9a
Fix code style.
chrneumann Aug 1, 2024
b104163
Revert "Fix render tests for terrain."
chrneumann Aug 1, 2024
644ae1f
Remove padding from camera elevation if inside terrain.
chrneumann Aug 1, 2024
5ee163c
Fix code style.
chrneumann Aug 1, 2024
50ccce6
Update expected bytes.
chrneumann Aug 1, 2024
35039dd
Merge branch 'main' into fix/keep-camera-outside-terrain
chrneumann Aug 1, 2024
518ce1f
Merge branch 'main' into fix/keep-camera-outside-terrain
HarelM Aug 1, 2024
cf3744f
Add Changelog entry.
chrneumann Aug 2, 2024
cd1f958
Merge branch 'main' into fix/keep-camera-outside-terrain
chrneumann Aug 2, 2024
9b7a3d4
Update Changelog.
chrneumann Aug 2, 2024
2b8f28b
Update Changelog.
chrneumann Aug 2, 2024
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 @@ -4,6 +4,7 @@
- _...Add new stuff here..._

### 🐞 Bug fixes
- Fix camera being able to move into 3D terrain ([#1542](https://github.com/maplibre/maplibre-gl-js/issues/1542))
- _...Add new stuff here..._

## 4.5.1
Expand Down
1 change: 1 addition & 0 deletions src/ui/camera.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ function createCamera(options?) {
.jumpTo(options);

camera._update = () => {};
camera._elevateCameraIfInsideTerrain = (_tr : any) => ({});

return camera;
}
Expand Down
98 changes: 68 additions & 30 deletions src/ui/camera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -958,10 +958,10 @@ export abstract class Camera extends Evented {
if (options.animate === false || (!options.essential && browser.prefersReducedMotion)) options.duration = 0;

const tr = this._getTransformForUpdate(),
startZoom = this.getZoom(),
startBearing = this.getBearing(),
startPitch = this.getPitch(),
startPadding = this.getPadding(),
startZoom = tr.zoom,
startBearing = tr.bearing,
startPitch = tr.pitch,
startPadding = tr.padding,

bearing = 'bearing' in options ? this._normalizeBearing(options.bearing, startBearing) : startBearing,
pitch = 'pitch' in options ? +options.pitch : startPitch,
Expand All @@ -975,7 +975,7 @@ export abstract class Camera extends Evented {
LngLat.convert(options.center || locationAtOffset),
options.zoom ?? startZoom
);
this._normalizeCenter(center);
this._normalizeCenter(center, tr);

const from = tr.project(locationAtOffset);
const delta = tr.project(center).sub(from);
Expand Down Expand Up @@ -1091,43 +1091,82 @@ export abstract class Camera extends Evented {
/**
* @internal
* Called when the camera is about to be manipulated.
* If `transformCameraUpdate` is specified, a copy of the current transform is created to track the accumulated changes.
* If `transformCameraUpdate` is specified or terrain is enabled, a copy of
* the current transform is created to track the accumulated changes.
* This underlying transform represents the "desired state" proposed by input handlers / animations / UI controls.
* It may differ from the state used for rendering (`this.transform`).
* @returns Transform to apply changes to
*/
_getTransformForUpdate(): Transform {
if (!this.transformCameraUpdate) return this.transform;
if (!this.transformCameraUpdate && !this.terrain) return this.transform;
chrneumann marked this conversation as resolved.
Show resolved Hide resolved

if (!this._requestedCameraState) {
this._requestedCameraState = this.transform.clone();
}
return this._requestedCameraState;
}

/**
* @internal
* Checks the given transform for the camera being below terrain surface and
* returns new pitch and zoom to fix that.
*
* With the new pitch and zoom, the camera will be at the same ground
* position but at higher altitude. It will still point to the same spot on
* the map.
*
* @param tr - The transform to check.
*/
_elevateCameraIfInsideTerrain(tr: Transform) : { pitch?: number; zoom?: number } {
const camera = tr.getCameraPosition();
const minAltitude = this.terrain.getElevationForLngLatZoom(camera.lngLat, tr.zoom);
if (camera.altitude < minAltitude) {
const newCamera = this.calculateCameraOptionsFromTo(
camera.lngLat, minAltitude, tr.center, tr.elevation);
return {
pitch: newCamera.pitch,
zoom: newCamera.zoom,
};
}
return {};
}

/**
* @internal
* Called after the camera is done being manipulated.
* @param tr - the requested camera end state
* If the camera is inside terrain, it gets elevated.
* Call `transformCameraUpdate` if present, and then apply the "approved" changes.
*/
_applyUpdatedTransform(tr: Transform) {
if (!this.transformCameraUpdate) return;

const nextTransform = tr.clone();
const {
center,
zoom,
pitch,
bearing,
elevation
} = this.transformCameraUpdate(nextTransform);
if (center) nextTransform.center = center;
if (zoom !== undefined) nextTransform.zoom = zoom;
if (pitch !== undefined) nextTransform.pitch = pitch;
if (bearing !== undefined) nextTransform.bearing = bearing;
if (elevation !== undefined) nextTransform.elevation = elevation;
this.transform.apply(nextTransform);
const modifiers : ((tr: Transform) => ReturnType<CameraUpdateTransformFunction>)[] = [];
if (this.terrain) {
modifiers.push(tr => this._elevateCameraIfInsideTerrain(tr));
}
if (this.transformCameraUpdate) {
modifiers.push(tr => this.transformCameraUpdate(tr));
}
if (!modifiers.length) {
return;
}
const finalTransform = tr.clone();
for (const modifier of modifiers) {
const nextTransform = finalTransform.clone();
const {
center,
zoom,
pitch,
bearing,
elevation
} = modifier(nextTransform);
if (center) nextTransform.center = center;
if (zoom !== undefined) nextTransform.zoom = zoom;
if (pitch !== undefined) nextTransform.pitch = pitch;
if (bearing !== undefined) nextTransform.bearing = bearing;
if (elevation !== undefined) nextTransform.elevation = elevation;
finalTransform.apply(nextTransform);
}
this.transform.apply(finalTransform);
}

_fireMoveEvents(eventData?: any) {
Expand Down Expand Up @@ -1232,10 +1271,10 @@ export abstract class Camera extends Evented {
}, options);

const tr = this._getTransformForUpdate(),
startZoom = this.getZoom(),
startBearing = this.getBearing(),
startPitch = this.getPitch(),
startPadding = this.getPadding();
startZoom = tr.zoom,
startBearing = tr.bearing,
startPitch = tr.pitch,
startPadding = tr.padding;

const bearing = 'bearing' in options ? this._normalizeBearing(options.bearing, startBearing) : startBearing;
const pitch = 'pitch' in options ? +options.pitch : startPitch;
Expand All @@ -1249,7 +1288,7 @@ export abstract class Camera extends Evented {
LngLat.convert(options.center || locationAtOffset),
options.zoom ?? startZoom
);
this._normalizeCenter(center);
this._normalizeCenter(center, tr);
const scale = tr.zoomScale(zoom - startZoom);

const from = tr.project(locationAtOffset);
Expand Down Expand Up @@ -1450,8 +1489,7 @@ export abstract class Camera extends Evented {

// If a path crossing the antimeridian would be shorter, extend the final coordinate so that
// interpolating between the two endpoints will cross it.
_normalizeCenter(center: LngLat) {
const tr = this.transform;
_normalizeCenter(center: LngLat, tr: Transform) {
if (!tr.renderWorldCopies || tr.lngRange) return;

const delta = center.lng - tr.center.lng;
Expand Down
3 changes: 3 additions & 0 deletions src/ui/handler/scroll_zoom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ describe('ScrollZoomHandler', () => {
browserNow.mockReturnValue(now);

const map = createMap();
map._elevateCameraIfInsideTerrain = (_tr : any) => ({});
map._renderTaskQueue.run();
map.terrain = {
pointCoordinate: () => null
Expand All @@ -329,6 +330,7 @@ describe('ScrollZoomHandler', () => {
browserNow.mockReturnValue(now);

let map = createMap();
map._elevateCameraIfInsideTerrain = (_tr : any) => ({});
map._renderTaskQueue.run();
map.terrain = {
pointCoordinate: () => null
Expand All @@ -353,6 +355,7 @@ describe('ScrollZoomHandler', () => {

// do the same test on the bottom
map = createMap();
map._elevateCameraIfInsideTerrain = (_tr : any) => ({});
map._renderTaskQueue.run();
map.terrain = {
pointCoordinate: () => null
Expand Down
5 changes: 3 additions & 2 deletions src/ui/handler/transform-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import {LngLat} from '../../geo/lng_lat';
/**
* @internal
* Shared utilities for the Handler classes to access the correct camera state.
* If Camera.transformCameraUpdate is specified, the "desired state" of camera may differ from the state used for rendering.
* The handlers need the "desired state" to track accumulated changes.
* If Camera.transformCameraUpdate is specified or terrain is enabled, the
* "desired state" of camera may differ from the state used for rendering. The
* handlers need the "desired state" to track accumulated changes.
*/
export class TransformProvider {
_map: Map;
Expand Down
33 changes: 33 additions & 0 deletions src/ui/map_tests/map_terrian.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,36 @@ describe('getCameraTargetElevation', () => {
expect(map.getCameraTargetElevation()).toBe(2000);
});
});

describe('Keep camera outside terrain', () => {
test('Try to move camera into terrain', () => {
const map = createMap();

let terrainElevation = 10;
const terrainStub = {} as Terrain;
terrainStub.getElevationForLngLatZoom = jest.fn(
(_lngLat: LngLat, _zoom: number) => terrainElevation
);
map.terrain = terrainStub;

// Terrain elevation is 10 everywhere, we are above it at zoom level 15
// with pitch 45 deg.
map.jumpTo({center: [0.0, 0.0], bearing: 0, pitch: 45, zoom: 15});
const initialCamPosition = map.transform.getCameraPosition();
expect(initialCamPosition.altitude).toBeCloseTo(506, 0);

// Now we set the elevation to 5000 everywhere and try to jump to the
// same position. This would lead to a jump into the terrain, which
// must not be possible.
// Camera should be above the terrain, but at the same location as
// before and with decreased pitch.
terrainElevation = 5000;
map.jumpTo({center: [0.0, 0.0], pitch: 45, zoom: 15});

expect(map.transform.getCameraPosition().lngLat.lng).toBeCloseTo(initialCamPosition.lngLat.lng);
expect(map.transform.getCameraPosition().lngLat.lat).toBeCloseTo(initialCamPosition.lngLat.lat);
expect(map.transform.pitch).toBeLessThan(45);
expect(map.transform.getCameraPosition().altitude).toBeGreaterThan(initialCamPosition.altitude);
expect(map.transform.getCameraPosition().altitude).toBeGreaterThan(terrainElevation);
});
});
2 changes: 1 addition & 1 deletion test/build/min.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('test min build', () => {
const decreaseQuota = 4096;

// feel free to update this value after you've checked that it has changed on purpose :-)
const expectedBytes = 800300;
const expectedBytes = 801147;

expect(actualBytes - expectedBytes).toBeLessThan(increaseQuota);
expect(expectedBytes - actualBytes).toBeLessThan(decreaseQuota);
Expand Down