diff --git a/modules/core/src/controllers/globe-controller.ts b/modules/core/src/controllers/globe-controller.ts index 4da90f2ed2b..a73ec9bdcde 100644 --- a/modules/core/src/controllers/globe-controller.ts +++ b/modules/core/src/controllers/globe-controller.ts @@ -6,19 +6,77 @@ import {clamp} from '@math.gl/core'; import Controller, {ControllerProps} from './controller'; import {MapState, MapStateProps} from './map-controller'; +import type {MapStateInternal} from './map-controller'; import {mod} from '../utils/math-utils'; import LinearInterpolator from '../transitions/linear-interpolator'; +import {zoomAdjust} from '../viewports/globe-viewport'; import {MAX_LATITUDE} from '@math.gl/web-mercator'; +type GlobeStateInternal = MapStateInternal & { + startPanPos?: [number, number]; +}; + class GlobeState extends MapState { - // Apply any constraints (mathematical or defined by _viewportProps) to map state + constructor( + options: MapStateProps & + GlobeStateInternal & { + makeViewport: (props: Record) => any; + } + ) { + const {startPanPos, ...mapStateOptions} = options; + super(mapStateOptions); + + if (startPanPos !== undefined) { + (this as any)._state.startPanPos = startPanPos; + } + } + + panStart({pos}: {pos: [number, number]}): GlobeState { + const {latitude, longitude, zoom} = this.getViewportProps(); + return this._getUpdatedState({ + startPanLngLat: [longitude, latitude], + startPanPos: pos, + startZoom: zoom + }) as GlobeState; + } + + pan({pos, startPos}: {pos: [number, number]; startPos?: [number, number]}): GlobeState { + const state = this.getState() as GlobeStateInternal; + const startPanLngLat = state.startPanLngLat || this._unproject(startPos); + if (!startPanLngLat) return this; + const startZoom = state.startZoom ?? this.getViewportProps().zoom; + const startPanPos = state.startPanPos || startPos; + + const coords = [startPanLngLat[0], startPanLngLat[1], startZoom]; + const viewport = this.makeViewport(this.getViewportProps()); + const newProps = viewport.panByPosition(coords, pos, startPanPos); + return this._getUpdatedState(newProps) as GlobeState; + } + + panEnd(): GlobeState { + return this._getUpdatedState({ + startPanLngLat: null, + startPanPos: null, + startZoom: null + }) as GlobeState; + } + + zoom({scale}: {scale: number}): MapState { + // In Globe view zoom does not take into account the mouse position + const startZoom = this.getState().startZoom || this.getViewportProps().zoom; + const zoom = startZoom + Math.log2(scale); + return this._getUpdatedState({zoom}); + } + applyConstraints(props: Required): Required { // Ensure zoom is within specified range - const {maxZoom, minZoom, zoom} = props; - props.zoom = clamp(zoom, minZoom, maxZoom); + const {longitude, latitude, maxZoom, minZoom, zoom} = props; + + const ZOOM0 = zoomAdjust(0); + const zoomAdjustment = zoomAdjust(latitude) - ZOOM0; + props.zoom = clamp(zoom, minZoom + zoomAdjustment, maxZoom + zoomAdjustment); - const {longitude, latitude} = props; if (longitude < -180 || longitude > 180) { props.longitude = mod(longitude + 180, 360) - 180; } diff --git a/modules/core/src/controllers/map-controller.ts b/modules/core/src/controllers/map-controller.ts index 880e0e6ab76..025aab23ed1 100644 --- a/modules/core/src/controllers/map-controller.ts +++ b/modules/core/src/controllers/map-controller.ts @@ -49,7 +49,7 @@ export type MapStateProps = { normalize?: boolean; }; -type MapStateInternal = { +export type MapStateInternal = { /** Interaction states, required to calculate change during transform */ /* The point on map being grabbed when the operation first started */ startPanLngLat?: [number, number]; diff --git a/modules/core/src/transitions/linear-interpolator.ts b/modules/core/src/transitions/linear-interpolator.ts index b064b9a743b..431c3e02dbc 100644 --- a/modules/core/src/transitions/linear-interpolator.ts +++ b/modules/core/src/transitions/linear-interpolator.ts @@ -5,7 +5,9 @@ import TransitionInterpolator from './transition-interpolator'; import {lerp} from '@math.gl/core'; +import log from '../utils/log'; import type Viewport from '../viewports/viewport'; +import GlobeViewport from '../viewports/globe-viewport'; const DEFAULT_PROPS = ['longitude', 'latitude', 'zoom', 'bearing', 'pitch']; const DEFAULT_REQUIRED_PROPS = ['longitude', 'latitude', 'zoom']; @@ -74,17 +76,23 @@ export default class LinearInterpolator extends TransitionInterpolator { const result = super.initializeProps(startProps, endProps); const {makeViewport, around} = this.opts; + if (makeViewport && around) { - const startViewport = makeViewport(startProps); - const endViewport = makeViewport(endProps); - const aroundPosition = startViewport.unproject(around); - result.start.around = around; - Object.assign(result.end, { - around: endViewport.project(aroundPosition), - aroundPosition, - width: endProps.width, - height: endProps.height - }); + const TestViewport = makeViewport(startProps); + if (TestViewport instanceof GlobeViewport) { + log.warn('around not supported in GlobeView')(); + } else { + const startViewport = makeViewport(startProps); + const endViewport = makeViewport(endProps); + const aroundPosition = startViewport.unproject(around); + result.start.around = around; + Object.assign(result.end, { + around: endViewport.project(aroundPosition), + aroundPosition, + width: endProps.width, + height: endProps.height + }); + } } return result; diff --git a/modules/core/src/viewports/globe-viewport.ts b/modules/core/src/viewports/globe-viewport.ts index 5a064a09e06..d06df5302b3 100644 --- a/modules/core/src/viewports/globe-viewport.ts +++ b/modules/core/src/viewports/globe-viewport.ts @@ -99,8 +99,7 @@ export default class GlobeViewport extends Viewport { // Exagerate distance by latitude to match the Web Mercator distortion // The goal is that globe and web mercator projection results converge at high zoom // https://github.com/maplibre/maplibre-gl-js/blob/f8ab4b48d59ab8fe7b068b102538793bbdd4c848/src/geo/projection/globe_transform.ts#L575-L577 - const scaleAdjust = 1 / Math.PI / Math.cos((latitude * Math.PI) / 180); - const scale = Math.pow(2, zoom) * scaleAdjust; + const scale = Math.pow(2, zoom - zoomAdjust(latitude)); const nearZ = opts.nearZ ?? nearZMultiplier; const farZ = opts.farZ ?? (altitude + (GLOBE_RADIUS * 2 * scale) / height) * farZMultiplier; @@ -232,15 +231,36 @@ export default class GlobeViewport extends Viewport { return xyz as [number, number]; } - panByPosition(coords: number[], pixel: number[]): GlobeViewportOptions { - const fromPosition = this.unproject(pixel); - return { - longitude: coords[0] - fromPosition[0] + this.longitude, - latitude: coords[1] - fromPosition[1] + this.latitude - }; + /** + * Pan the globe using delta-based movement + * @param coords - the geographic coordinates where the pan started + * @param pixel - the current screen position + * @param startPixel - the screen position where the pan started + * @returns updated viewport options with new longitude/latitude + */ + panByPosition( + [startLng, startLat, startZoom]: number[], + pixel: number[], + startPixel: number[] + ): GlobeViewportOptions { + // Scale rotation speed inversely with zoom, to approximate constant panning speed + const scale = Math.pow(2, this.zoom - zoomAdjust(this.latitude)); + const rotationSpeed = 0.25 / scale; + + const longitude = startLng + rotationSpeed * (startPixel[0] - pixel[0]); + let latitude = startLat - rotationSpeed * (startPixel[1] - pixel[1]); + latitude = Math.max(Math.min(latitude, MAX_LATITUDE), -MAX_LATITUDE); + const out = {longitude, latitude, zoom: startZoom - zoomAdjust(startLat)}; + out.zoom += zoomAdjust(out.latitude); + return out; } } +export function zoomAdjust(latitude: number): number { + const scaleAdjust = Math.PI * Math.cos((latitude * Math.PI) / 180); + return Math.log2(scaleAdjust); +} + function transformVector(matrix: number[], vector: number[]): number[] { const result = vec4.transformMat4([], vector, matrix); vec4.scale(result, result, 1 / result[3]); diff --git a/modules/core/src/viewports/orbit-viewport.ts b/modules/core/src/viewports/orbit-viewport.ts index 0e615c68d4a..94a673ded4b 100644 --- a/modules/core/src/viewports/orbit-viewport.ts +++ b/modules/core/src/viewports/orbit-viewport.ts @@ -148,7 +148,7 @@ export default class OrbitViewport extends Viewport { return [X, Y, Z]; } - panByPosition(coords: number[], pixel: number[]): OrbitViewportOptions { + panByPosition(coords: number[], pixel: number[], startPixel?: number[]): OrbitViewportOptions { const p0 = this.project(coords); const nextCenter = [ this.width / 2 + p0[0] - pixel[0], diff --git a/modules/core/src/viewports/orthographic-viewport.ts b/modules/core/src/viewports/orthographic-viewport.ts index bef4a6ff856..ce5325d39e4 100644 --- a/modules/core/src/viewports/orthographic-viewport.ts +++ b/modules/core/src/viewports/orthographic-viewport.ts @@ -148,7 +148,11 @@ export default class OrthographicViewport extends Viewport { } /* Needed by LinearInterpolator */ - panByPosition(coords: number[], pixel: number[]): OrthographicViewportOptions { + panByPosition( + coords: number[], + pixel: number[], + startPixel?: number[] + ): OrthographicViewportOptions { const fromLocation = pixelsToWorld(pixel, this.pixelUnprojectionMatrix); const toLocation = this.projectFlat(coords); diff --git a/modules/core/src/viewports/viewport.ts b/modules/core/src/viewports/viewport.ts index e80a2e96962..50588b63aca 100644 --- a/modules/core/src/viewports/viewport.ts +++ b/modules/core/src/viewports/viewport.ts @@ -408,9 +408,10 @@ export default class Viewport { * * @param {Array} coords - world coordinates * @param {Array} pixel - [x,y] coordinates on screen + * @param {Array} startPixel - [x,y] screen position where pan started (optional, for delta-based panning) * @return {Object} props of the new viewport */ - panByPosition(coords: number[], pixel: number[]): any { + panByPosition(coords: number[], pixel: number[], startPixel?: number[]): any { return null; } diff --git a/modules/core/src/viewports/web-mercator-viewport.ts b/modules/core/src/viewports/web-mercator-viewport.ts index ccc8d3f94df..12275738e86 100644 --- a/modules/core/src/viewports/web-mercator-viewport.ts +++ b/modules/core/src/viewports/web-mercator-viewport.ts @@ -270,7 +270,11 @@ export default class WebMercatorViewport extends Viewport { return addMetersToLngLat(lngLatZ, xyz); } - panByPosition(coords: number[], pixel: number[]): WebMercatorViewportOptions { + panByPosition( + coords: number[], + pixel: number[], + startPixel?: number[] + ): WebMercatorViewportOptions { const fromLocation = pixelsToWorld(pixel, this.pixelUnprojectionMatrix); const toLocation = this.projectFlat(coords);