-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
Alternate zoom with terrain #12354
Alternate zoom with terrain #12354
Changes from 45 commits
1159341
5364b04
47504b8
b01337e
5e01266
da3d155
b5cb3ac
e590e8e
7ad12dc
f434e74
9556be9
5c70f09
59c4b2a
2636f8f
30e461f
619f578
2e7e89e
4001079
f159782
62e080f
6fb34fe
7454aef
9818d69
793f74d
8c1de95
821aa0e
51fb421
1759ba5
fa64d10
6f2355a
1cac447
9890b39
3796928
71814dc
1ec5bd6
9683c27
01536c1
20bbce5
7775495
b51a88a
e40ef59
a4bf61c
2afa50d
b061c73
af15e40
fdfeac2
b790159
96f5c5d
34d921e
6020e65
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -149,6 +149,7 @@ class Transform { | |
_nearZ: number; | ||
_farZ: number; | ||
_mercatorScaleRatio: number; | ||
_isCameraConstrained: boolean; | ||
|
||
constructor(minZoom: ?number, maxZoom: ?number, minPitch: ?number, maxPitch: ?number, renderWorldCopies: boolean | void, projection?: ?ProjectionSpecification, bounds: ?LngLatBounds) { | ||
this.tileSize = 512; // constant | ||
|
@@ -225,13 +226,14 @@ class Transform { | |
this._updateCameraOnTerrain(); | ||
this._calcMatrices(); | ||
} | ||
updateElevation(constrainCameraOverTerrain: boolean) { // On render, no need for higher granularity on update reasons. | ||
|
||
updateElevation(constrainCameraOverTerrain: boolean, isDragging: boolean = false) { | ||
const centerAltitudeChanged = this._elevation && this._elevation.exaggeration() !== this._centerAltitudeValidForExaggeration; | ||
if (this._seaLevelZoom == null || centerAltitudeChanged) { | ||
this._updateCameraOnTerrain(); | ||
} | ||
if (constrainCameraOverTerrain || centerAltitudeChanged) { | ||
this._constrainCameraAltitude(); | ||
this._constrainCamera(isDragging); | ||
} | ||
this._calcMatrices(); | ||
} | ||
|
@@ -1586,7 +1588,7 @@ class Transform { | |
} | ||
} | ||
|
||
_constrainCameraAltitude() { | ||
_constrainCamera(isDragging: boolean = false) { | ||
if (!this._elevation) | ||
return; | ||
|
||
|
@@ -1597,30 +1599,35 @@ class Transform { | |
// The default camera position might have been compensated by the active projection model. | ||
const mercPixelsPerMeter = mercatorZfromAltitude(1, this._center.lat) * this.worldSize; | ||
const pos = this._computeCameraPosition(mercPixelsPerMeter); | ||
|
||
const elevationAtCamera = elevation.getAtPointOrZero(new MercatorCoordinate(...pos)); | ||
const minHeight = this._minimumHeightOverTerrain() * Math.cos(degToRad(this._maxPitch)); | ||
|
||
const minHeight = this._minimumHeightOverTerrain(); | ||
const terrainElevation = this.pixelsPerMeter / this.worldSize * elevationAtCamera; | ||
const cameraHeight = this._camera.position[2] - terrainElevation; | ||
|
||
if (cameraHeight < minHeight) { | ||
const center = this.locationCoordinate(this._center, this._centerAltitude); | ||
const cameraToCenter = [center.x - pos[0], center.y - pos[1], center.z - pos[2]]; | ||
const prevDistToCamera = vec3.length(cameraToCenter); | ||
// If camera is under terrain or dragging at unsafe distance from terrain, force camera position above terrain | ||
if (cameraHeight <= 0 || isDragging) { | ||
const center = this.locationCoordinate(this._center, this._centerAltitude); | ||
const cameraToCenter = [center.x - pos[0], center.y - pos[1], center.z - pos[2]]; | ||
|
||
// Adjust the camera vector so that the camera is placed above the terrain. | ||
// Distance between the camera and the center point is kept constant. | ||
cameraToCenter[2] -= (minHeight - cameraHeight) / this._pixelsPerMercatorPixel; | ||
const prevDistToCamera = vec3.length(cameraToCenter); | ||
// Adjust the camera vector so that the camera is placed above the terrain. | ||
// Distance between the camera and the center point is kept constant. | ||
cameraToCenter[2] -= (minHeight - cameraHeight) / this._pixelsPerMercatorPixel; | ||
const newDistToCamera = vec3.length(cameraToCenter); | ||
|
||
const newDistToCamera = vec3.length(cameraToCenter); | ||
if (newDistToCamera === 0) | ||
return; | ||
if (newDistToCamera === 0) | ||
return; | ||
|
||
vec3.scale(cameraToCenter, cameraToCenter, prevDistToCamera / newDistToCamera * this._pixelsPerMercatorPixel); | ||
this._camera.position = [center.x - cameraToCenter[0], center.y - cameraToCenter[1], center.z * this._pixelsPerMercatorPixel - cameraToCenter[2]]; | ||
vec3.scale(cameraToCenter, cameraToCenter, prevDistToCamera / newDistToCamera * this._pixelsPerMercatorPixel); | ||
this._camera.position = [center.x - cameraToCenter[0], center.y - cameraToCenter[1], center.z * this._pixelsPerMercatorPixel - cameraToCenter[2]]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also not introduced in this PR, but I'm curious as to why we adjust the camera along the vector of camera to center? Why not simply raise the camera`s z value? |
||
this._updateStateFromCamera(); | ||
|
||
this._camera.orientation = orientationFromFrame(cameraToCenter, this._camera.up()); | ||
this._updateStateFromCamera(); | ||
// Set camera as constrained to keep zoom at safe distance from terrain | ||
} else { | ||
this._isCameraConstrained = true; | ||
} | ||
} | ||
} | ||
|
||
|
@@ -1681,7 +1688,7 @@ class Transform { | |
this.zoom += this.scaleZoom(s); | ||
} | ||
|
||
this._constrainCameraAltitude(); | ||
this._constrainCamera(); | ||
this._unmodified = unmodified; | ||
this._constraining = false; | ||
} | ||
|
@@ -1968,10 +1975,10 @@ class Transform { | |
|
||
_minimumHeightOverTerrain(): number { | ||
// Determine minimum height for the camera over the terrain related to current zoom. | ||
// Values above than 2 allow max-pitch camera closer to e.g. top of the hill, exposing | ||
// Values above 4 allow camera closer to e.g. top of the hill, exposing | ||
// drape raster overscale artifacts or cut terrain (see under it) as it gets clipped on | ||
// near plane. Returned value is in mercator coordinates. | ||
const MAX_DRAPE_OVERZOOM = 2; | ||
const MAX_DRAPE_OVERZOOM = 4; | ||
const zoom = Math.min((this._seaLevelZoom != null ? this._seaLevelZoom : this._zoom) + MAX_DRAPE_OVERZOOM, this._maxZoom); | ||
return this._mercatorZfromZoom(zoom); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -125,11 +125,18 @@ class KeyboardHandler { | |
return { | ||
cameraAnimation: (map: Map) => { | ||
const zoom = map.getZoom(); | ||
|
||
// Camera is constrained to control zooming too close to terrain | ||
if (map.transform._isCameraConstrained) { | ||
if (zoomDir && zoomDir > 0) zoomDir = 0; | ||
if (yDir && yDir < 0) yDir = 0; | ||
map.transform._isCameraConstrained = false; | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm concerned that including this logic in the keyboard handler introduces excess complexity and fragility. Could we have the same behavior on all There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the feedback - I'll look into There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Would it be possible to decide if a movement is allowed or not allowed from with Do you think that we could change the behavior of Or is it important for user experience that we allow zooming to a higher zoom level while constraining panning to a lower one? (The latter may well be the case, both mouse interactions seem to work well in this PR) Besides code readability and maintainability, another reason why I think separating interaction and camera adjustment code would be preferable is that it's hard to predict how users will interact with camera position. For instance with |
||
map.easeTo({ | ||
duration: 300, | ||
easeId: 'keyboardHandler', | ||
easing: easeOut, | ||
|
||
zoom: zoomDir ? Math.round(zoom) + zoomDir * (e.shiftKey ? 2 : 1) : zoom, | ||
bearing: map.getBearing() + bearingDir * this._bearingStep, | ||
pitch: map.getPitch() + pitchDir * this._pitchStep, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
{ | ||
"version": 8, | ||
"metadata": { | ||
"test": { | ||
"height": 256, | ||
"width": 256 | ||
} | ||
}, | ||
"center": [ | ||
-104.93611234965806, | ||
38.85724324489064 | ||
], | ||
"zoom": 22, | ||
"pitch": 85, | ||
"bearing": -79, | ||
"terrain": { | ||
"source": "rgbterrain", | ||
"exaggeration": 1 | ||
}, | ||
"sources": { | ||
"rgbterrain": { | ||
"type": "raster-dem", | ||
"tiles": [ | ||
"local://tiles/12-759-1609.terrain.png" | ||
], | ||
"maxzoom": 11, | ||
"tileSize": 256 | ||
} | ||
}, | ||
"glyphs": "local://glyphs/{fontstack}/{range}.pbf", | ||
"layers": [ | ||
{ | ||
"id": "background", | ||
"type": "background", | ||
"paint": { | ||
"background-color": "gray" | ||
} | ||
}, | ||
{ | ||
"id": "hillshade-translucent", | ||
"type": "hillshade", | ||
"source": "rgbterrain", | ||
"paint": { | ||
"hillshade-exaggeration": 1 | ||
} | ||
} | ||
] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there more situations where we need to run this path? I'm not sure whether this should be tied to the drag gesture and instead run always. While testing the PR, I've noticed that when the camera has some inertia due to a click-drag-release gesture, it results in the camera getting too close to the terrain, then when picking it back up with a drag gesture the constraint gets applied again resulting in an immediate camera jump.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we run it always we experience a weird affect of jumping while panning/zooming, but it felt more intuitive with dragging and allows for the user to keep moving forward in the terrain by dragging if they zoom too close. I'll check out the mouse codepath that @SnailBones suggested. Thanks for the feedback and testing it out @karimnaaji!