diff --git a/modules/core/src/index.ts b/modules/core/src/index.ts index ef4463aab19..536710f5af2 100644 --- a/modules/core/src/index.ts +++ b/modules/core/src/index.ts @@ -38,7 +38,11 @@ export {default as DeckRenderer} from './lib/deck-renderer'; // Viewports export {default as Viewport} from './viewports/viewport'; -export {default as WebMercatorViewport} from './viewports/web-mercator-viewport'; +export { + default as WebMercatorViewport, + getZoomFromElevation, + getElevationFromZoom +} from './viewports/web-mercator-viewport'; export {default as _GlobeViewport} from './viewports/globe-viewport'; export {default as OrbitViewport} from './viewports/orbit-viewport'; export {default as OrthographicViewport} from './viewports/orthographic-viewport'; diff --git a/modules/core/src/viewports/web-mercator-viewport.ts b/modules/core/src/viewports/web-mercator-viewport.ts index 12275738e86..70d53ca9389 100644 --- a/modules/core/src/viewports/web-mercator-viewport.ts +++ b/modules/core/src/viewports/web-mercator-viewport.ts @@ -14,6 +14,7 @@ import { getProjectionParameters, altitudeToFovy, fovyToAltitude, + getMeterZoom, fitBounds, getBounds } from '@math.gl/web-mercator'; @@ -324,3 +325,61 @@ export default class WebMercatorViewport extends Viewport { return new WebMercatorViewport({width, height, longitude, latitude, zoom}); } } + +/** + * Returns the zoom level that will position the camera at the given elevation above the ground. + * Can be used to create a WebMercatorViewport from a camera at a known physical elevation (e.g. for 3D tileset traversal). + * + * @param options + * @param options.elevation - Physical camera elevation in meters above the ground + * @param options.latitude - Latitude of the viewport center in degrees + * @param options.height - Height of the viewport in pixels + * @param options.pitch - Tilt of the camera in degrees. Default `0` + * @param options.fovy - Camera field of view in degrees. If provided, overrides `altitude` + * @param options.altitude - Camera altitude relative to the viewport height. Default `1.5` + * @returns Zoom level for use in WebMercatorViewport + */ +export function getZoomFromElevation(options: { + elevation: number; + latitude: number; + height: number; + pitch?: number; + fovy?: number; + altitude?: number; +}): number { + const {elevation, latitude, height, pitch = 0, fovy, altitude = 1.5} = options; + const altitudeRatio = fovy ? fovyToAltitude(fovy) : altitude; + return ( + getMeterZoom({latitude}) + + Math.log2((altitudeRatio * Math.cos(pitch * (Math.PI / 180)) * height) / elevation) + ); +} + +/** + * Returns the camera elevation in meters for the given zoom level. + * This is the inverse of `getZoomFromElevation`. + * + * @param options + * @param options.zoom - Zoom level + * @param options.latitude - Latitude of the viewport center in degrees + * @param options.height - Height of the viewport in pixels + * @param options.pitch - Tilt of the camera in degrees. Default `0` + * @param options.fovy - Camera field of view in degrees. If provided, overrides `altitude` + * @param options.altitude - Camera altitude relative to the viewport height. Default `1.5` + * @returns Camera elevation in meters above the ground + */ +export function getElevationFromZoom(options: { + zoom: number; + latitude: number; + height: number; + pitch?: number; + fovy?: number; + altitude?: number; +}): number { + const {zoom, latitude, height, pitch = 0, fovy, altitude = 1.5} = options; + const altitudeRatio = fovy ? fovyToAltitude(fovy) : altitude; + return ( + (altitudeRatio * Math.cos(pitch * (Math.PI / 180)) * height) / + Math.pow(2, zoom - getMeterZoom({latitude})) + ); +} diff --git a/modules/main/src/index.ts b/modules/main/src/index.ts index 04e8fe76983..13467925fed 100644 --- a/modules/main/src/index.ts +++ b/modules/main/src/index.ts @@ -33,6 +33,9 @@ export { OrbitViewport, OrthographicViewport, FirstPersonViewport, + // Viewport utilities + getZoomFromElevation, + getElevationFromZoom, // Controllers Controller, MapController, diff --git a/test/modules/core/viewports/web-mercator-viewport.spec.ts b/test/modules/core/viewports/web-mercator-viewport.spec.ts index 22aace0052e..f899d19b8da 100644 --- a/test/modules/core/viewports/web-mercator-viewport.spec.ts +++ b/test/modules/core/viewports/web-mercator-viewport.spec.ts @@ -4,7 +4,7 @@ import test from 'tape-promise/tape'; import {equals, config, Vector3} from '@math.gl/core'; -import {WebMercatorViewport} from 'deck.gl'; +import {WebMercatorViewport, getZoomFromElevation, getElevationFromZoom} from 'deck.gl'; import {Matrix4} from '@math.gl/core'; // Adjust sensitivity of math.gl's equals @@ -304,6 +304,52 @@ test('WebMercatorViewport#constructor#fovy', t => { t.end(); }); +test('getZoomFromElevation and getElevationFromZoom', t => { + const testCases = [ + {latitude: 0, height: 600, altitude: 1.5, elevation: 1000}, + {latitude: 45, height: 600, altitude: 1.5, elevation: 5000}, + {latitude: 60, height: 800, altitude: 2.0, elevation: 500}, + {latitude: 37.8, height: 600, altitude: 1.5, elevation: 10000, pitch: 30}, + {latitude: 37.8, height: 600, fovy: 50, elevation: 2000} + ]; + + for (const tc of testCases) { + const zoom = getZoomFromElevation(tc); + t.ok(Number.isFinite(zoom), `getZoomFromElevation returns a number for ${JSON.stringify(tc)}`); + + const roundtrippedElevation = getElevationFromZoom({...tc, zoom}); + t.ok( + Math.abs(roundtrippedElevation - tc.elevation) < 1e-6, + `getElevationFromZoom is the inverse of getZoomFromElevation for ${JSON.stringify(tc)}` + ); + } + + // Verify the zoom produces the correct camera height in the viewport + const latitude = 37.8; + const height = 600; + const altitude = 1.5; + const desiredElevation = 5000; + const zoom = getZoomFromElevation({elevation: desiredElevation, latitude, height, altitude}); + const viewport = new WebMercatorViewport({ + latitude, + longitude: -122, + zoom, + height, + width: 800, + altitude + }); + const cameraZ = viewport.cameraPosition[2]; + // cameraPosition[2] is in common space; convert to meters + const distanceScales = viewport.getDistanceScales(); + const cameraElevationMeters = cameraZ / distanceScales.unitsPerMeter[2]; + t.ok( + Math.abs(cameraElevationMeters - desiredElevation) / desiredElevation < 0.01, + 'camera elevation matches desired elevation (within 1%)' + ); + + t.end(); +}); + function getCulling(p, planes) { let outDir = null; p = new Vector3(p);