Skip to content

Commit

Permalink
Add support for cameraForBounds on globe projection (#12138)
Browse files Browse the repository at this point in the history
* Integrating frustum fit prototype for `cameraForBounds` API

- Account for aspect ratio

- Adjust for aspect ratio contribution

- Support for pitch

- Support on mercator

- Add debug page

Cleanup

Cleanup

Update _cameraForBoxAndBearing with new technique

Update _cameraForBoxAndBearing with new technique for globe

Account for transition

Account for option max zoom

Cleanups

Fix case when there is no sphere intersection

* Simplifications

* Simplifications

* Use tighter AABB constructed from camera orientation

* Simplifications from latest change

* Address FIXME wrt fixed pixel space conversion

Fixup

* Remove unused

* Flow and lint

* Reintroduce padding options

Apply center offset

* Fix unit tests

* Adding tests

* Add more unit test

* Address review comments

* Address review comments

* Update src/ui/camera.js

Co-authored-by: Volodymyr Agafonkin <agafonkin@gmail.com>

* Address review comments

* Address review comments

* Update src/geo/projection/globe_util.js

Co-authored-by: Volodymyr Agafonkin <agafonkin@gmail.com>

* Update src/ui/camera.js

Co-authored-by: Volodymyr Agafonkin <agafonkin@gmail.com>

* Apply review comment

* Address review comment

* Fix flow

Co-authored-by: Volodymyr Agafonkin <agafonkin@gmail.com>
  • Loading branch information
karimnaaji and mourner authored Sep 6, 2022
1 parent 25922d3 commit ca2f99f
Show file tree
Hide file tree
Showing 11 changed files with 514 additions and 71 deletions.
4 changes: 3 additions & 1 deletion debug/7517.html
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@
container: 'map',
style: 'mapbox://styles/mapbox/streets-v9',
center: [-68.13734351262877, 45.137451890638886],
zoom: 5
zoom: 5,
hash: true,
projection: 'globe'
});

map.on('load', function() {
Expand Down
124 changes: 124 additions & 0 deletions debug/camera-for-bounds.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<!DOCTYPE html>
<html>
<head>
<title>Mapbox GL JS debug page</title>
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel='stylesheet' href='../dist/mapbox-gl.css' />
<script src="https://unpkg.com/@turf/turf@6/turf.min.js"></script>
<script src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-draw/v1.2.2/mapbox-gl-draw.js"></script>
<link rel="stylesheet" href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-draw/v1.2.2/mapbox-gl-draw.css" type="text/css">
<style>
body { margin: 0; padding: 0; }
html, body, #map { height: 100%; }
#debugcanvas {
position:absolute;
left: 0px;
top: 0px;
pointer-events:none;
}
.btn-control {
font: bold 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
background-color: white;
color: #000;
position: absolute;
top: 10px;
left: 50%;
z-index: 1;
border: none;
width: 200px;
margin-left: -100px;
display: block;
cursor: pointer;
padding: 10px 20px;
border-radius: 3px;
}
</style>
</head>

<body>
<div id='map'></div>
<button id="zoomto" class="btn-control">Zoom to bounds</button>

<script src='../dist/mapbox-gl-dev.js'></script>
<script src='../debug/access_token_generated.js'></script>
<script>

var map = window.map = new mapboxgl.Map({
container: 'map',
zoom: 5,
center: [-68.13734351262877, 45.137451890638886],
style: 'mapbox://styles/mapbox/dark-v10',
hash: true,
projection: 'globe'
});

/*global MapboxDraw, turf*/

const draw = new MapboxDraw({
displayControlsDefault: false,
controls: {
polygon: true,
trash: true
},
defaultMode: 'draw_polygon'
});
map.addControl(draw);

map.on('draw.create', updatePolygon);
map.on('draw.delete', updatePolygon);
map.on('draw.update', updatePolygon);

const data = {
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": []
}
};

let bbox;

function updatePolygon(e) {
bbox = turf.bbox(draw.getAll());
const bboxpolygon = [];
bboxpolygon.push([bbox[0], bbox[1]]); // x0, y0
bboxpolygon.push([bbox[2], bbox[1]]); // x1, y0
bboxpolygon.push([bbox[2], bbox[3]]); // x1, y1
bboxpolygon.push([bbox[0], bbox[3]]); // x0, y1
bboxpolygon.push([bbox[0], bbox[1]]); // x0, y0
data.geometry.coordinates[0] = bboxpolygon;
map.getSource('bbox').setData(data);
}

map.on('load', function() {
map.addLayer({
'id': 'bbox',
'type': 'line',
'source': {
'type': 'geojson',
data
},
'layout': {},
'paint': {
'line-color': 'red',
'line-width': 2
}
});

map.setFog({});

document.getElementById('zoomto').addEventListener('click', () => {
const bounds = [
[bbox[0], bbox[1]],
[bbox[2], bbox[3]]
];
const camera = map.cameraForBounds(bounds, {bearing: map.getBearing()});
map.easeTo(camera);
});
});

</script>
</body>
</html>
4 changes: 3 additions & 1 deletion flow-typed/gl-matrix.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ declare module "gl-matrix" {
normalize<T: Vec3>(T, Vec3): T,
add<T: Vec3>(T, Vec3, Vec3): T,
sub<T: Vec3>(T, Vec3, Vec3): T,
set<T: Vec3>(T, number, number, number): T,
subtract<T: Vec3>(T, Vec3, Vec3): T,
cross<T: Vec3>(T, Vec3, Vec3): T,
negate<T: Vec3>(T, Vec3): T,
Expand All @@ -52,7 +53,8 @@ declare module "gl-matrix" {
declare var vec4: {
scale<T: Vec4>(T, Vec4, number): T,
mul<T: Vec4>(T, Vec4, Vec4): T,
transformMat4<T: Vec4>(T, Vec4, Mat4): T
transformMat4<T: Vec4>(T, Vec4, Mat4): T,
normalize<T: Vec4>(T, Vec4): T
};

declare var mat2: {
Expand Down
2 changes: 1 addition & 1 deletion src/geo/projection/globe.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ export default class Globe extends Mercator {
// Instead, use a fixed reference latitude at lower zoom levels. And transition between
// this latitude and the center's latitude as you zoom in. This is a compromise that
// makes globe view more usable with existing camera parameters, styles and data.
const referenceScale = mercatorZfromAltitude(1, GLOBE_SCALE_MATCH_LATITUDE) * worldSize;
const centerScale = mercatorZfromAltitude(1, lat) * worldSize;
const referenceScale = mercatorZfromAltitude(1, GLOBE_SCALE_MATCH_LATITUDE) * worldSize;
const combinedScale = interpolate(referenceScale, centerScale, interpolationT);
return this.pixelsPerMeter(lat, worldSize) / combinedScale;
}
Expand Down
8 changes: 8 additions & 0 deletions src/geo/projection/globe_util.js
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,14 @@ function csLatLngToECEF(cosLat: number, sinLat: number, lng: number, radius: num
return [sx, sy, sz];
}

export function ecefToLatLng([x, y, z]: Array<number>): LngLat {
const radius = Math.hypot(x, y, z);
const lng = Math.atan2(x, z);
const lat = Math.PI * 0.5 - Math.acos(-y / radius);

return new LngLat(radToDeg(lng), radToDeg(lat));
}

export function latLngToECEF(lat: number, lng: number, radius?: number): Vec3 {
assert(lat <= 90 && lat >= -90, 'Lattitude must be between -90 and 90');
return csLatLngToECEF(Math.cos(degToRad(lat)), Math.sin(degToRad(lat)), lng, radius);
Expand Down
57 changes: 51 additions & 6 deletions src/geo/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import type {PaddingOptions} from './edge_insets.js';
import type Tile from '../source/tile.js';
import type {ProjectionSpecification} from '../style-spec/types.js';
import type {FeatureDistanceData} from '../style-spec/feature_filter/index.js';
import type {Vec3, Vec4, Quat} from 'gl-matrix';
import type {Mat4, Vec3, Vec4, Quat} from 'gl-matrix';

const NUM_WORLD_COPIES = 3;
const DEFAULT_MIN_ZOOM = 0;
Expand Down Expand Up @@ -196,6 +196,7 @@ class Transform {
clone._centerAltitude = this._centerAltitude;
clone._centerAltitudeValidForExaggeration = this._centerAltitudeValidForExaggeration;
clone.tileSize = this.tileSize;
clone.mercatorFromTransition = this.mercatorFromTransition;
clone.width = this.width;
clone.height = this.height;
clone.cameraElevationReference = this.cameraElevationReference;
Expand Down Expand Up @@ -374,14 +375,24 @@ class Transform {
this._calcMatrices();
}

get fov(): number {
return this._fov / Math.PI * 180;
get aspect(): number {
return this.width / this.height;
}

get fovX(): number {
return this._fov;
}

get fovY(): number {
const focalLength = 1.0 / Math.tan(this.fovX * 0.5);
return 2 * Math.atan((1.0 / this.aspect) / focalLength);
}

set fov(fov: number) {
fov = Math.max(0.01, Math.min(60, fov));
if (this._fov === fov) return;
this._unmodified = false;
this._fov = fov / 180 * Math.PI;
this._fov = degToRad(fov);
this._calcMatrices();
}

Expand Down Expand Up @@ -1969,6 +1980,29 @@ class Transform {
return this.scaleZoom(this.cameraToCenterDistance / (z * this.tileSize));
}

// This function is helpful to approximate true zoom given a mercator height with varying ppm.
// With Globe, since we use a fixed reference latitude at lower zoom levels and transition between this
// latitude and the center's latitude as you zoom in, camera to center distance varies dynamically.
// As the cameraToCenterDistance is a function of zoom, we need to approximate the true zoom
// given a mercator meter value in order to eliminate the zoom/cameraToCenterDistance dependency.
zoomFromMercatorZAdjusted(z: number): number {
const getZoom = (zoom) => {
const d = this.getCameraToCenterDistance(this.projection, zoom);
return this.scaleZoom(d / (z * this.tileSize));
};

let zoom = getZoom(this.zoom);
let diff = Math.abs(zoom - getZoom(zoom));
let lastdiff;
while (lastdiff !== diff) {
zoom = getZoom(zoom);
lastdiff = diff;
diff = Math.abs(zoom - getZoom(zoom));
}

return zoom;
}

_terrainEnabled(): boolean {
if (!this._elevation) return false;
if (!this.projection.supportsTerrain) {
Expand Down Expand Up @@ -2075,11 +2109,22 @@ class Transform {
}
}

getCameraToCenterDistance(projection: Projection): number {
const t = getProjectionInterpolationT(projection, this.zoom, this.width, this.height, 1024);
getCameraToCenterDistance(projection: Projection, zoom: number = this.zoom): number {
const t = getProjectionInterpolationT(projection, zoom, this.width, this.height, 1024);
const projectionScaler = projection.pixelSpaceConversion(this.center.lat, this.worldSize, t);
return 0.5 / Math.tan(this._fov * 0.5) * this.height * projectionScaler;
}

getWorldToCameraMatrix(): Mat4 {
const zUnit = this.projection.zAxisUnit === "meters" ? this.pixelsPerMeter : 1.0;
const worldToCamera = this._camera.getWorldToCamera(this.worldSize, zUnit);

if (this.projection.name === 'globe') {
mat4.multiply(worldToCamera, worldToCamera, this.globeMatrix);
}

return worldToCamera;
}
}

export default Transform;
Loading

0 comments on commit ca2f99f

Please sign in to comment.