33// Copyright (c) vis.gl contributors
44
55import TransitionInterpolator from './transition-interpolator' ;
6- import { lerp } from '@math.gl/core' ;
6+ import { clamp , lerp , vec3 } from '@math.gl/core' ;
77
8+ import type Viewport from '../viewports/viewport' ;
9+ import GlobeViewport , { zoomAdjust } from '../viewports/globe-viewport' ;
10+ import { Globe } from '../viewports/globe-utils' ;
811import { flyToViewport , getFlyToDuration } from '@math.gl/web-mercator' ;
912
1013const LINEARLY_INTERPOLATED_PROPS = {
@@ -17,6 +20,105 @@ const DEFAULT_OPTS = {
1720 curve : 1.414
1821} ;
1922
23+ type FlyToInterpolatorOptions = {
24+ /** The zooming "curve" that will occur along the flight path. Default 1.414 */
25+ curve ?: number ;
26+ /** The average speed of the animation defined in relation to `options.curve`, it linearly affects the duration, higher speed returns smaller durations and vice versa. Default 1.2 */
27+ speed ?: number ;
28+ /** The average speed of the animation measured in screenfuls per second. Similar to `opts.speed` it linearly affects the duration, when specified `opts.speed` is ignored. */
29+ screenSpeed ?: number ;
30+ /** Maximum duration in milliseconds, if calculated duration exceeds this value, `0` is returned. */
31+ maxDuration ?: number ;
32+ /** Construct the active viewport for the current view state. Injected by controllers when available. */
33+ makeViewport ?: ( props : Record < string , any > ) => Viewport | null | undefined ;
34+ } ;
35+
36+ type FlyToViewportProps = {
37+ width : number ;
38+ height : number ;
39+ longitude : number ;
40+ latitude : number ;
41+ zoom : number ;
42+ bearing ?: number ;
43+ pitch ?: number ;
44+ } ;
45+
46+ function getFlyToViewportProps ( props : Record < string , any > ) : FlyToViewportProps {
47+ return {
48+ width : props . width ,
49+ height : props . height ,
50+ longitude : props . longitude ,
51+ latitude : props . latitude ,
52+ zoom : props . zoom ,
53+ bearing : props . bearing ,
54+ pitch : props . pitch
55+ } ;
56+ }
57+
58+ function getFlyToOptions (
59+ opts : FlyToInterpolator [ 'opts' ]
60+ ) : Omit < FlyToInterpolatorOptions , 'makeViewport' > {
61+ const { makeViewport, ...flyToOptions } = opts ;
62+ return flyToOptions ;
63+ }
64+
65+ function isGlobeViewportTransition (
66+ startProps : Record < string , any > ,
67+ endProps : Record < string , any > ,
68+ makeViewport ?: ( props : Record < string , any > ) => Viewport | null | undefined
69+ ) : boolean {
70+ if ( ! makeViewport ) {
71+ return false ;
72+ }
73+
74+ return (
75+ makeViewport ( startProps ) instanceof GlobeViewport ||
76+ makeViewport ( endProps ) instanceof GlobeViewport
77+ ) ;
78+ }
79+
80+ function normalizeAngle ( value : number ) : number {
81+ return ( ( ( ( value + 180 ) % 360 ) + 360 ) % 360 ) - 180 ;
82+ }
83+
84+ function lerpAngle ( start : number , end : number , t : number ) : number {
85+ return normalizeAngle ( start + normalizeAngle ( end - start ) * t ) ;
86+ }
87+
88+ function slerpPosition ( start : number [ ] , end : number [ ] , t : number ) : number [ ] {
89+ const dot = clamp ( vec3 . dot ( start , end ) , - 1 , 1 ) ;
90+ const omega = Math . acos ( dot ) ;
91+
92+ if ( omega < 1e-6 ) {
93+ return vec3 . normalize (
94+ [ ] ,
95+ [
96+ lerp ( start [ 0 ] , end [ 0 ] , t ) ,
97+ lerp ( start [ 1 ] , end [ 1 ] , t ) ,
98+ lerp ( start [ 2 ] , end [ 2 ] , t )
99+ ]
100+ ) as number [ ] ;
101+ }
102+
103+ const sinOmega = Math . sin ( omega ) ;
104+ if ( Math . abs ( sinOmega ) < 1e-6 ) {
105+ let axis = vec3 . cross ( [ ] , start , [ 0 , 0 , 1 ] ) ;
106+ if ( vec3 . len ( axis ) < 1e-6 ) {
107+ axis = vec3 . cross ( [ ] , start , [ 0 , 1 , 0 ] ) ;
108+ }
109+ vec3 . normalize ( axis , axis ) ;
110+ return Globe . rotate ( start , axis , Math . PI * t ) ;
111+ }
112+
113+ const startScale = Math . sin ( ( 1 - t ) * omega ) / sinOmega ;
114+ const endScale = Math . sin ( t * omega ) / sinOmega ;
115+ return [
116+ start [ 0 ] * startScale + end [ 0 ] * endScale ,
117+ start [ 1 ] * startScale + end [ 1 ] * endScale ,
118+ start [ 2 ] * startScale + end [ 2 ] * endScale
119+ ] ;
120+ }
121+
20122/**
21123 * This class adapts mapbox-gl-js Map#flyTo animation so it can be used in
22124 * react/redux architecture.
@@ -25,25 +127,10 @@ const DEFAULT_OPTS = {
25127 * "Jarke J. van Wijk and Wim A.A. Nuij"
26128 */
27129export default class FlyToInterpolator extends TransitionInterpolator {
28- opts : {
29- curve : number ;
30- speed : number ;
31- screenSpeed ?: number ;
32- maxDuration ?: number ;
33- } ;
130+ opts : Required < Pick < FlyToInterpolatorOptions , 'curve' | 'speed' > > &
131+ Omit < FlyToInterpolatorOptions , 'curve' | 'speed' > ;
34132
35- constructor (
36- opts : {
37- /** The zooming "curve" that will occur along the flight path. Default 1.414 */
38- curve ?: number ;
39- /** The average speed of the animation defined in relation to `options.curve`, it linearly affects the duration, higher speed returns smaller durations and vice versa. Default 1.2 */
40- speed ?: number ;
41- /** The average speed of the animation measured in screenfuls per second. Similar to `opts.speed` it linearly affects the duration, when specified `opts.speed` is ignored. */
42- screenSpeed ?: number ;
43- /** Maximum duration in milliseconds, if calculated duration exceeds this value, `0` is returned. */
44- maxDuration ?: number ;
45- } = { }
46- ) {
133+ constructor ( opts : FlyToInterpolatorOptions = { } ) {
47134 super ( {
48135 compare : [ 'longitude' , 'latitude' , 'zoom' , 'bearing' , 'pitch' , 'position' ] ,
49136 extract : [ 'width' , 'height' , 'longitude' , 'latitude' , 'zoom' , 'bearing' , 'pitch' , 'position' ] ,
@@ -53,30 +140,55 @@ export default class FlyToInterpolator extends TransitionInterpolator {
53140 }
54141
55142 interpolateProps ( startProps , endProps , t ) {
56- const viewport = flyToViewport ( startProps , endProps , t , this . opts ) ;
143+ const flyToOptions = getFlyToOptions ( this . opts ) ;
144+ const useGlobeViewport = isGlobeViewportTransition ( startProps , endProps , this . opts . makeViewport ) ;
145+ const viewport = useGlobeViewport
146+ ? this . _interpolateGlobeProps ( startProps , endProps , t , flyToOptions )
147+ : flyToViewport ( startProps , endProps , t , flyToOptions ) ;
57148
58149 // Linearly interpolate 'bearing', 'pitch' and 'position'.
59150 // If they are not supplied, they are interpreted as zeros in viewport calculation
60151 // (fallback defined in WebMercatorViewport)
61152 // Because there is no guarantee that the current controller's ViewState normalizes
62153 // these props, safe guard is needed to avoid generating NaNs
63154 for ( const key in LINEARLY_INTERPOLATED_PROPS ) {
64- viewport [ key ] = lerp (
65- startProps [ key ] || LINEARLY_INTERPOLATED_PROPS [ key ] ,
66- endProps [ key ] || LINEARLY_INTERPOLATED_PROPS [ key ] ,
67- t
68- ) ;
155+ const startValue = startProps [ key ] || LINEARLY_INTERPOLATED_PROPS [ key ] ;
156+ const endValue = endProps [ key ] || LINEARLY_INTERPOLATED_PROPS [ key ] ;
157+ viewport [ key ] =
158+ useGlobeViewport && key === 'bearing'
159+ ? lerpAngle ( startValue , endValue , t )
160+ : lerp ( startValue , endValue , t ) ;
69161 }
70162
71163 return viewport ;
72164 }
73165
166+ private _interpolateGlobeProps ( startProps , endProps , t , flyToOptions ) {
167+ const startPosition = Globe . toPosition ( startProps . longitude , startProps . latitude ) ;
168+ const endPosition = Globe . toPosition ( endProps . longitude , endProps . latitude ) ;
169+ const [ longitude , latitude ] = Globe . toLngLat ( slerpPosition ( startPosition , endPosition , t ) ) ;
170+ const flyTo = flyToViewport (
171+ getFlyToViewportProps ( startProps ) ,
172+ getFlyToViewportProps ( endProps ) ,
173+ t ,
174+ flyToOptions
175+ ) ;
176+ const flyToScaleZoom = flyTo . zoom - zoomAdjust ( flyTo . latitude , true ) ;
177+ const zoom = flyToScaleZoom + zoomAdjust ( latitude , true ) ;
178+
179+ return {
180+ longitude,
181+ latitude,
182+ zoom
183+ } ;
184+ }
185+
74186 // computes the transition duration
75187 getDuration ( startProps , endProps ) {
76188 let { transitionDuration} = endProps ;
77189 if ( transitionDuration === 'auto' ) {
78190 // auto calculate duration based on start and end props
79- transitionDuration = getFlyToDuration ( startProps , endProps , this . opts ) ;
191+ transitionDuration = getFlyToDuration ( startProps , endProps , getFlyToOptions ( this . opts ) ) ;
80192 }
81193 return transitionDuration ;
82194 }
0 commit comments