diff --git a/modules/core/src/controllers/globe-controller.ts b/modules/core/src/controllers/globe-controller.ts index ea7ef052161..18f704ddacb 100644 --- a/modules/core/src/controllers/globe-controller.ts +++ b/modules/core/src/controllers/globe-controller.ts @@ -9,6 +9,9 @@ import {MapState, MapStateProps} from './map-controller'; import {mod} from '../utils/math-utils'; import LinearInterpolator from '../transitions/linear-interpolator'; +// matches Web Mercator projection limit +const MAX_VALID_LATITUDE = 85.051129; + class GlobeState extends MapState { // Apply any constraints (mathematical or defined by _viewportProps) to map state applyConstraints(props: Required): Required { @@ -20,7 +23,7 @@ class GlobeState extends MapState { if (longitude < -180 || longitude > 180) { props.longitude = mod(longitude + 180, 360) - 180; } - props.latitude = clamp(latitude, -89, 89); + props.latitude = clamp(latitude, -MAX_VALID_LATITUDE, MAX_VALID_LATITUDE); return props; } diff --git a/modules/core/src/viewports/globe-viewport.ts b/modules/core/src/viewports/globe-viewport.ts index 8db91f50f0a..d9411315738 100644 --- a/modules/core/src/viewports/globe-viewport.ts +++ b/modules/core/src/viewports/globe-viewport.ts @@ -5,6 +5,7 @@ import {Matrix4} from '@math.gl/core'; import Viewport from './viewport'; import {PROJECTION_MODE} from '../lib/constants'; +import {altitudeToFovy, fovyToAltitude} from '@math.gl/web-mercator'; import {vec3, vec4} from '@math.gl/core'; @@ -50,6 +51,8 @@ export type GlobeViewportOptions = { zoom?: number; /** Use orthographic projection */ orthographic?: boolean; + /** Camera fovy in degrees. If provided, overrides `altitude` */ + fovy?: number; /** Scaler for the near plane, 1 unit equals to the height of the viewport. Default `0.1` */ nearZMultiplier?: number; /** Scaler for the far plane, 1 unit equals to the distance from the camera to the edge of the screen. Default `2` */ @@ -59,37 +62,39 @@ export type GlobeViewportOptions = { }; export default class GlobeViewport extends Viewport { - // @ts-ignore - longitude: number; - // @ts-ignore - latitude: number; - resolution: number; + longitude!: number; + latitude!: number; + resolution!: number; constructor(opts: GlobeViewportOptions = {}) { const { latitude = 0, longitude = 0, zoom = 0, - nearZMultiplier = 0.1, - farZMultiplier = 2, + nearZMultiplier = 0.5, + farZMultiplier = 1, resolution = 10 } = opts; - let {height, altitude = 1.5} = opts; + let {height, altitude = 1.5, fovy} = opts; height = height || 1; - altitude = Math.max(0.75, altitude); + if (fovy) { + altitude = fovyToAltitude(fovy); + } else { + fovy = altitudeToFovy(altitude); + } + // Used to match globe and web mercator projection at high zoom + const scaleAdjust = 1 / Math.PI / Math.cos((latitude * Math.PI) / 180); + const scale = Math.pow(2, zoom) * scaleAdjust; + const farZ = altitude + (GLOBE_RADIUS * 2 * scale) / height; // Calculate view matrix const viewMatrix = new Matrix4().lookAt({eye: [0, -altitude, 0], up: [0, 0, 1]}); - const scale = Math.pow(2, zoom); viewMatrix.rotateX(latitude * DEGREES_TO_RADIANS); viewMatrix.rotateZ(-longitude * DEGREES_TO_RADIANS); viewMatrix.scale(scale / height); - const halfFov = Math.atan(0.5 / altitude); - const relativeScale = (GLOBE_RADIUS * 2 * scale) / height; - super({ ...opts, // x, y, width, @@ -103,12 +108,13 @@ export default class GlobeViewport extends Viewport { // projection matrix parameters distanceScales: getDistanceScales(), - fovyRadians: halfFov * 2, + fovy, focalDistance: altitude, near: nearZMultiplier, - far: Math.min(2, 1 / relativeScale + 1) * altitude * farZMultiplier + far: farZ * farZMultiplier }); + this.scale = scale; this.latitude = latitude; this.longitude = longitude; this.resolution = resolution; diff --git a/modules/core/src/views/first-person-view.ts b/modules/core/src/views/first-person-view.ts index ba693901d53..b1510f7ab3f 100644 --- a/modules/core/src/views/first-person-view.ts +++ b/modules/core/src/views/first-person-view.ts @@ -45,7 +45,7 @@ export default class FirstPersonView extends View { super(props); } - get ViewportType() { - return GlobeViewport; + getViewportType(viewState: GlobeViewState) { + return viewState.zoom > 12 ? WebMercatorViewport : GlobeViewport; } get ControllerType() { diff --git a/modules/core/src/views/map-view.ts b/modules/core/src/views/map-view.ts index 156dc0fcd8c..4d80ef73cc3 100644 --- a/modules/core/src/views/map-view.ts +++ b/modules/core/src/views/map-view.ts @@ -55,7 +55,7 @@ export default class MapView extends View { super(props); } - get ViewportType() { + getViewportType() { return WebMercatorViewport; } diff --git a/modules/core/src/views/orbit-view.ts b/modules/core/src/views/orbit-view.ts index ec06b81e5b6..8b9a42c36de 100644 --- a/modules/core/src/views/orbit-view.ts +++ b/modules/core/src/views/orbit-view.ts @@ -48,7 +48,7 @@ export default class OrbitView extends View { this.props.orbitAxis = props.orbitAxis || 'Z'; } - get ViewportType() { + getViewportType() { return OrbitViewport; } diff --git a/modules/core/src/views/orthographic-view.ts b/modules/core/src/views/orthographic-view.ts index fb56e04ee3a..902f3a08f27 100644 --- a/modules/core/src/views/orthographic-view.ts +++ b/modules/core/src/views/orthographic-view.ts @@ -34,7 +34,7 @@ export default class OrthographicView extends View = CommonViewProps > { id: string; - abstract get ViewportType(): ConstructorOf; + abstract getViewportType(viewState: ViewState): ConstructorOf; protected abstract get ControllerType(): ConstructorOf>; private _x: Position; @@ -102,7 +102,7 @@ export default abstract class View< } // To correctly compare padding use depth=2 - return this.ViewportType === view.ViewportType && deepEqual(this.props, view.props, 2); + return this.constructor === view.constructor && deepEqual(this.props, view.props, 2); } /** Make viewport from canvas dimensions and view state */ @@ -114,7 +114,8 @@ export default abstract class View< if (!viewportDimensions.height || !viewportDimensions.width) { return null; } - return new this.ViewportType({...viewState, ...this.props, ...viewportDimensions}); + const ViewportType = this.getViewportType(viewState); + return new ViewportType({...viewState, ...this.props, ...viewportDimensions}); } getViewStateId(): string { diff --git a/test/modules/core/viewports/globe-viewport.spec.ts b/test/modules/core/viewports/globe-viewport.spec.ts index bb23354e816..52e1a1bace0 100644 --- a/test/modules/core/viewports/globe-viewport.spec.ts +++ b/test/modules/core/viewports/globe-viewport.spec.ts @@ -17,7 +17,7 @@ const TEST_VIEWPORTS = [ { width: 800, height: 600, - latitude: 90, + latitude: 80, longitude: 0, zoom: 1 } diff --git a/test/modules/geo-layers/tileset-2d/utils.spec.ts b/test/modules/geo-layers/tileset-2d/utils.spec.ts index ac7d998af0d..a64504ef4af 100644 --- a/test/modules/geo-layers/tileset-2d/utils.spec.ts +++ b/test/modules/geo-layers/tileset-2d/utils.spec.ts @@ -275,7 +275,7 @@ const TEST_CASES = [ viewState: { longitude: -6, latitude: 58, - zoom: 1.5 + zoom: 1.5 + 0.7353406094252244 // Math.log2(1 / Math.PI / Math.cos(58 / 180 * Math.PI)) } }), tileSize: 512, diff --git a/test/render/golden-images/globe-mvt.png b/test/render/golden-images/globe-mvt.png index 7b65b742a16..b04885b4a4c 100644 Binary files a/test/render/golden-images/globe-mvt.png and b/test/render/golden-images/globe-mvt.png differ diff --git a/test/render/golden-images/path-globe.png b/test/render/golden-images/path-globe.png index d7a67867ee0..5bff0e6f618 100644 Binary files a/test/render/golden-images/path-globe.png and b/test/render/golden-images/path-globe.png differ diff --git a/test/render/golden-images/polygon-globe-extruded.png b/test/render/golden-images/polygon-globe-extruded.png index a6aa97004da..da689e5c18f 100644 Binary files a/test/render/golden-images/polygon-globe-extruded.png and b/test/render/golden-images/polygon-globe-extruded.png differ diff --git a/test/render/golden-images/polygon-globe.png b/test/render/golden-images/polygon-globe.png index a48dfd7c72d..4babe09c9b0 100644 Binary files a/test/render/golden-images/polygon-globe.png and b/test/render/golden-images/polygon-globe.png differ diff --git a/test/render/test-cases/path-layer.js b/test/render/test-cases/path-layer.js index 713e57554c3..41f2f88cd5c 100644 --- a/test/render/test-cases/path-layer.js +++ b/test/render/test-cases/path-layer.js @@ -293,7 +293,7 @@ export default [ viewState: { latitude: 0, longitude: 0, - zoom: 0 + zoom: 1.5 }, layers: [ new PathLayer({ diff --git a/test/render/test-cases/polygon-layer.js b/test/render/test-cases/polygon-layer.js index 258b3c28eb1..7f8fc74bf15 100644 --- a/test/render/test-cases/polygon-layer.js +++ b/test/render/test-cases/polygon-layer.js @@ -149,7 +149,7 @@ export default [ viewState: { latitude: 0, longitude: 50, - zoom: 0 + zoom: 1.5 }, layers: [ new PolygonLayer({ @@ -184,7 +184,7 @@ export default [ viewState: { latitude: 0, longitude: 50, - zoom: 0 + zoom: 1.5 }, layers: [ new PolygonLayer({ diff --git a/test/render/test-cases/views.js b/test/render/test-cases/views.js index fcd4c38437e..78734a18b34 100644 --- a/test/render/test-cases/views.js +++ b/test/render/test-cases/views.js @@ -101,9 +101,7 @@ export default [ viewState: { longitude: -100, latitude: 80, - zoom: 0, - pitch: 0, - bearing: 0 + zoom: -1 }, layers: [ new SimpleMeshLayer({