mirror of
https://github.com/visgl/react-map-gl.git
synced 2026-01-25 16:02:50 +00:00
Viewport flyTo Interpolation: add support for auto duration and other options. (#866)
* Viewport flyTo Interpoloation: add support for auto duration, curve, speed, screenSpeed and maxDuration
This commit is contained in:
parent
f0218d9009
commit
eea2841052
@ -112,6 +112,7 @@ Remarks:
|
||||
+ `transitionEasing`
|
||||
+ `transitionInterruption`
|
||||
- The default interaction/transition behavior can always be intercepted and overwritten in the handler for `onViewportChange`. However, if a transition is in progress, the properties that are being transitioned (e.g. longitude and latitude) should not be manipulated, otherwise the change will be interpreted as an interruption of the transition.
|
||||
- When using `FlyToInterpolator` for `transitionInterpolator`, `transitionDuration` can be set to `'auto'` where actual duration is auto calculated based on start and end viewports and is linear to the distance between them. This duration can be further customized using `speed` parameter to `FlyToInterpolator` constructor.
|
||||
|
||||
|
||||
## Transition Interpolators
|
||||
|
||||
@ -15,9 +15,16 @@ import ReactMapGL, {FlyToInterpolator} from 'react-map-gl';
|
||||
|
||||
##### constructor
|
||||
|
||||
`new FlyToInterpolator()`
|
||||
`new FlyToInterpolator([options])`
|
||||
|
||||
Parameters:
|
||||
- `options` {Object} (optional)
|
||||
+ `curve` (Number, optional, default: 1.414) - The zooming "curve" that will occur along the flight path.
|
||||
- `speed` (Number, optional, default: 1.2) - 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.
|
||||
- `screenSpeed` (Number, optional) - 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.
|
||||
- `maxDuration` (Number, optional) - Maximum duration in milliseconds, if calculated duration exceeds this value, `0` is returned.
|
||||
|
||||
|
||||
|
||||
## Source
|
||||
[viewport-fly-to-interpolator.js](https://github.com/uber/react-map-gl/tree/4.1-release/src/utils/transition/viewport-fly-to-interpolator.js)
|
||||
|
||||
|
||||
@ -27,8 +27,8 @@ export default class App extends Component {
|
||||
longitude,
|
||||
latitude,
|
||||
zoom: 11,
|
||||
transitionInterpolator: new FlyToInterpolator(),
|
||||
transitionDuration: 3000
|
||||
transitionInterpolator: new FlyToInterpolator({speed: 2}),
|
||||
transitionDuration: 'auto'
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -11,26 +11,43 @@ type Viewport = {
|
||||
bearing: number
|
||||
};
|
||||
|
||||
type FlyToInterpolatorOpts = {
|
||||
curve?: number,
|
||||
speed?: number,
|
||||
screenSpeed?: number,
|
||||
maxDuraiton?: number
|
||||
};
|
||||
|
||||
declare module 'viewport-mercator-project' {
|
||||
declare export class WebMercatorViewport {
|
||||
constructor(Viewport) : WebMercatorViewport;
|
||||
constructor(Viewport): WebMercatorViewport;
|
||||
|
||||
width: number,
|
||||
height: number,
|
||||
longitude: number,
|
||||
latitude: number,
|
||||
zoom: number,
|
||||
pitch: number,
|
||||
bearing: number,
|
||||
width: number;
|
||||
height: number;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
zoom: number;
|
||||
pitch: number;
|
||||
bearing: number;
|
||||
|
||||
project(xyz: Array<number>): Array<number>;
|
||||
unproject(xyz: Array<number>): Array<number>;
|
||||
getMapCenterByLngLatPosition({lngLat: Array<number>, pos: Array<number>}): Array<number>;
|
||||
fitBounds(bounds: [[Number,Number],[Number,Number]], options: any): WebMercatorViewport;
|
||||
fitBounds(bounds: [[Number, Number], [Number, Number]], options: any): WebMercatorViewport;
|
||||
}
|
||||
|
||||
declare export function normalizeViewportProps(props: Viewport) : Viewport;
|
||||
declare export function flyToViewport(startProps: Viewport, endProps: Viewport, t: number) : Viewport;
|
||||
declare export function normalizeViewportProps(props: Viewport): Viewport;
|
||||
declare export function flyToViewport(
|
||||
startProps: Viewport,
|
||||
endProps: Viewport,
|
||||
t: number,
|
||||
opts?: FlyToInterpolatorOpts
|
||||
): Viewport;
|
||||
declare export function getFlyToDuration(
|
||||
startProps: Viewport,
|
||||
endProps: Viewport,
|
||||
opts?: FlyToInterpolatorOpts
|
||||
): number;
|
||||
|
||||
declare export default typeof WebMercatorViewport;
|
||||
}
|
||||
|
||||
@ -48,7 +48,7 @@
|
||||
"mjolnir.js": "^2.2.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"viewport-mercator-project": "^6.1.0"
|
||||
"viewport-mercator-project": "^6.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.4.4",
|
||||
|
||||
@ -39,7 +39,7 @@ const propTypes = Object.assign({}, StaticMap.propTypes, {
|
||||
|
||||
/** Viewport transition **/
|
||||
// transition duration for viewport change
|
||||
transitionDuration: PropTypes.number,
|
||||
transitionDuration: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
// TransitionInterpolator instance, can be used to perform custom transitions.
|
||||
transitionInterpolator: PropTypes.object,
|
||||
// type of interruption of current transition on update.
|
||||
|
||||
@ -20,8 +20,9 @@
|
||||
// THE SOFTWARE.
|
||||
import {createElement, createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import BaseControl from './base-control';
|
||||
import type {MjolnirEvent} from 'mjolnir.js';
|
||||
|
||||
import BaseControl from './base-control';
|
||||
import {getDynamicPosition, ANCHOR_POSITION} from '../utils/dynamic-position';
|
||||
|
||||
import type {BaseControlProps} from './base-control';
|
||||
@ -159,7 +160,7 @@ export default class Popup extends BaseControl<PopupProps, *, HTMLDivElement> {
|
||||
return style;
|
||||
}
|
||||
|
||||
_onClick = evt => {
|
||||
_onClick = (evt: MjolnirEvent) => {
|
||||
if (this.props.captureClick) {
|
||||
evt.stopPropagation();
|
||||
}
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
// @flow
|
||||
const EPSILON = 1e-9;
|
||||
const EPSILON = 1e-7;
|
||||
|
||||
// Returns true if value is either an array or a typed array
|
||||
function isArray(value: any): boolean {
|
||||
return Array.isArray(value) || ArrayBuffer.isView(value);
|
||||
}
|
||||
|
||||
// TODO: use math.gl
|
||||
export function equals(a: any, b: any): boolean {
|
||||
if (a === b) {
|
||||
return true;
|
||||
|
||||
@ -133,7 +133,10 @@ export default class TransitionManager {
|
||||
}
|
||||
|
||||
_isTransitionEnabled(props: ViewportProps): boolean {
|
||||
return props.transitionDuration > 0 && Boolean(props.transitionInterpolator);
|
||||
const {transitionDuration, transitionInterpolator} = props;
|
||||
return (
|
||||
(transitionDuration > 0 || transitionDuration === 'auto') && Boolean(transitionInterpolator)
|
||||
);
|
||||
}
|
||||
|
||||
_isUpdateDueToCurrentTransition(props: ViewportProps): boolean {
|
||||
@ -170,6 +173,10 @@ export default class TransitionManager {
|
||||
cancelAnimationFrame(this._animationFrame);
|
||||
}
|
||||
|
||||
// update transitionDuration for 'auto' mode
|
||||
const {transitionInterpolator} = endProps;
|
||||
const duration = transitionInterpolator.getDuration(startProps, endProps);
|
||||
|
||||
const initialProps = endProps.transitionInterpolator.initializeProps(startProps, endProps);
|
||||
|
||||
const interactionState = {
|
||||
@ -182,7 +189,7 @@ export default class TransitionManager {
|
||||
|
||||
this.state = {
|
||||
// Save current transition props
|
||||
duration: endProps.transitionDuration,
|
||||
duration,
|
||||
easing: endProps.transitionEasing,
|
||||
interpolator: endProps.transitionInterpolator,
|
||||
interruption: endProps.transitionInterruption,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
import {equals} from '../math-utils';
|
||||
import assert from '../assert';
|
||||
import type {MapStateProps} from '../map-state';
|
||||
|
||||
export default class TransitionInterpolator {
|
||||
propNames: Array<string> = [];
|
||||
@ -47,4 +48,14 @@ export default class TransitionInterpolator {
|
||||
interpolateProps(startProps: any, endProps: any, t: number): any {
|
||||
assert(false, 'interpolateProps is not implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns transition duration
|
||||
* @param startProps {object} - a list of starting viewport props
|
||||
* @param endProps {object} - a list of target viewport props
|
||||
* @returns {Number} - transition duration in milliseconds
|
||||
*/
|
||||
getDuration(startProps: MapStateProps, endProps: MapStateProps) {
|
||||
return endProps.transitionDuration;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import assert from '../assert';
|
||||
import TransitionInterpolator from './transition-interpolator';
|
||||
|
||||
import {flyToViewport} from 'viewport-mercator-project';
|
||||
import {flyToViewport, getFlyToDuration} from 'viewport-mercator-project';
|
||||
import {isValid, getEndValueByShortestPath} from './transition-utils';
|
||||
import {lerp} from '../math-utils';
|
||||
|
||||
@ -11,6 +11,18 @@ import type {MapStateProps} from '../map-state';
|
||||
const VIEWPORT_TRANSITION_PROPS = ['longitude', 'latitude', 'zoom', 'bearing', 'pitch'];
|
||||
const REQUIRED_PROPS = ['latitude', 'longitude', 'zoom', 'width', 'height'];
|
||||
const LINEARLY_INTERPOLATED_PROPS = ['bearing', 'pitch'];
|
||||
const DEFAULT_OPTS = {
|
||||
speed: 1.2,
|
||||
curve: 1.414
|
||||
// screenSpeed and maxDuration are used only if specified
|
||||
};
|
||||
|
||||
type FlyToInterpolatorProps = {
|
||||
curve?: number,
|
||||
speed?: number,
|
||||
screenSpeed?: number,
|
||||
maxDuraiton?: number
|
||||
};
|
||||
|
||||
/**
|
||||
* This class adapts mapbox-gl-js Map#flyTo animation so it can be used in
|
||||
@ -20,8 +32,24 @@ const LINEARLY_INTERPOLATED_PROPS = ['bearing', 'pitch'];
|
||||
* "Jarke J. van Wijk and Wim A.A. Nuij"
|
||||
*/
|
||||
export default class ViewportFlyToInterpolator extends TransitionInterpolator {
|
||||
speed: number;
|
||||
propNames = VIEWPORT_TRANSITION_PROPS;
|
||||
|
||||
/**
|
||||
* @param props {Object}
|
||||
- `props.curve` (Number, optional, default: 1.414) - The zooming "curve" that will occur along the flight path.
|
||||
- `props.speed` (Number, optional, default: 1.2) - 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.
|
||||
- `props.screenSpeed` (Number, optional) - 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.
|
||||
- `props.maxDuration` (Number, optional) - Maximum duration in milliseconds, if calculated duration exceeds this value, `0` is returned.
|
||||
*/
|
||||
constructor(props: FlyToInterpolatorProps = {}) {
|
||||
super();
|
||||
|
||||
this.props = Object.assign({}, DEFAULT_OPTS, props);
|
||||
}
|
||||
|
||||
props: FlyToInterpolatorProps;
|
||||
|
||||
initializeProps(startProps: MapStateProps, endProps: MapStateProps) {
|
||||
const startViewportProps = {};
|
||||
const endViewportProps = {};
|
||||
@ -49,7 +77,7 @@ export default class ViewportFlyToInterpolator extends TransitionInterpolator {
|
||||
}
|
||||
|
||||
interpolateProps(startProps: MapStateProps, endProps: MapStateProps, t: number) {
|
||||
const viewport = flyToViewport(startProps, endProps, t);
|
||||
const viewport = flyToViewport(startProps, endProps, t, this.props);
|
||||
|
||||
// Linearly interpolate 'bearing' and 'pitch' if exist.
|
||||
for (const key of LINEARLY_INTERPOLATED_PROPS) {
|
||||
@ -58,4 +86,14 @@ export default class ViewportFlyToInterpolator extends TransitionInterpolator {
|
||||
|
||||
return viewport;
|
||||
}
|
||||
|
||||
// computes the transition duration
|
||||
getDuration(startProps: MapStateProps, endProps: MapStateProps) {
|
||||
let {transitionDuration} = endProps;
|
||||
if (transitionDuration === 'auto') {
|
||||
// auto calculate duration based on start and end props
|
||||
transitionDuration = getFlyToDuration(startProps, endProps, this.props);
|
||||
}
|
||||
return transitionDuration;
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,7 +48,7 @@ const TEST_CASES = [
|
||||
zoom: 12,
|
||||
pitch: 0,
|
||||
bearing: 0,
|
||||
transitionDuration: 200
|
||||
transitionDuration: 'auto'
|
||||
},
|
||||
// transitionDuration is 0
|
||||
{
|
||||
@ -109,9 +109,21 @@ const TEST_CASES = [
|
||||
pitch: 0,
|
||||
bearing: 0,
|
||||
transitionDuration: 200
|
||||
},
|
||||
// viewport change interrupting transition
|
||||
{
|
||||
width: 100,
|
||||
height: 100,
|
||||
longitude: -122.45,
|
||||
latitude: 37.78,
|
||||
zoom: 12,
|
||||
pitch: 0,
|
||||
bearing: 0,
|
||||
transitionInterpolator: new ViewportFlyToInterpolator({speed: 50}),
|
||||
transitionDuration: 'auto'
|
||||
}
|
||||
],
|
||||
expect: [true, true]
|
||||
expect: [true, true, true]
|
||||
}
|
||||
];
|
||||
|
||||
@ -185,8 +197,8 @@ test('TransitionManager#callbacks', t => {
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
t.is(startCount, 2, 'onTransitionStart() called twice');
|
||||
t.is(interruptCount, 1, 'onTransitionInterrupt() called once');
|
||||
t.is(startCount, 3, 'onTransitionStart() called twice');
|
||||
t.is(interruptCount, 2, 'onTransitionInterrupt() called once');
|
||||
t.is(endCount, 1, 'onTransitionEnd() called once');
|
||||
t.ok(updateCount > 2, 'onViewportChange() called');
|
||||
t.end();
|
||||
@ -416,3 +428,36 @@ test('TransitionManager#TRANSITION_EVENTS', t => {
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('TransitionManager#auto#duration', t => {
|
||||
const mergeProps = props => Object.assign({}, TransitionManager.defaultProps, props);
|
||||
const initialProps = {
|
||||
width: 100,
|
||||
height: 100,
|
||||
longitude: -122.45,
|
||||
latitude: 37.78,
|
||||
zoom: 12,
|
||||
pitch: 0,
|
||||
bearing: 0,
|
||||
transitionDuration: 200
|
||||
};
|
||||
const transitionManager = new TransitionManager(mergeProps(initialProps));
|
||||
transitionManager.processViewportChange(
|
||||
mergeProps({
|
||||
width: 100,
|
||||
height: 100,
|
||||
longitude: -100.45, // changed
|
||||
latitude: 37.78,
|
||||
zoom: 12,
|
||||
pitch: 0,
|
||||
bearing: 0,
|
||||
transitionInterpolator: new ViewportFlyToInterpolator(),
|
||||
transitionDuration: 'auto'
|
||||
})
|
||||
);
|
||||
t.ok(
|
||||
Number.isFinite(transitionManager.state.duration) && transitionManager.state.duration > 0,
|
||||
'should set duraiton when using "auto" mode'
|
||||
);
|
||||
t.end();
|
||||
});
|
||||
|
||||
@ -2,6 +2,21 @@ import test from 'tape-catch';
|
||||
import {ViewportFlyToInterpolator} from 'react-map-gl/utils/transition';
|
||||
import {toLowPrecision} from 'react-map-gl/test/test-utils';
|
||||
|
||||
const START_PROPS = {
|
||||
width: 800,
|
||||
height: 600,
|
||||
longitude: -122.45,
|
||||
latitude: 37.78,
|
||||
zoom: 12
|
||||
};
|
||||
|
||||
const END_PROPS = {
|
||||
width: 800,
|
||||
height: 600,
|
||||
longitude: -74,
|
||||
latitude: 40.7,
|
||||
zoom: 11
|
||||
};
|
||||
/* eslint-disable max-len */
|
||||
const TEST_CASES = [
|
||||
{
|
||||
@ -12,20 +27,8 @@ const TEST_CASES = [
|
||||
},
|
||||
{
|
||||
title: 'optional prop fallback',
|
||||
startProps: {
|
||||
width: 800,
|
||||
height: 600,
|
||||
longitude: -122.45,
|
||||
latitude: 37.78,
|
||||
zoom: 12
|
||||
},
|
||||
endProps: {
|
||||
width: 800,
|
||||
height: 600,
|
||||
longitude: -74,
|
||||
latitude: 40.7,
|
||||
zoom: 11
|
||||
},
|
||||
startProps: START_PROPS,
|
||||
endProps: END_PROPS,
|
||||
expect: {
|
||||
start: {
|
||||
width: 800,
|
||||
@ -136,7 +139,32 @@ const TEST_CASES = [
|
||||
];
|
||||
/* eslint-enable max-len */
|
||||
|
||||
test('LinearInterpolator#initializeProps', t => {
|
||||
const DURATION_TEST_CASES = [
|
||||
{
|
||||
title: 'fixed duration',
|
||||
endProps: {transitionDuration: 100},
|
||||
expected: 100
|
||||
},
|
||||
{
|
||||
title: 'auto duration',
|
||||
endProps: {transitionDuration: 'auto'},
|
||||
expected: 7325.794
|
||||
},
|
||||
{
|
||||
title: 'high speed',
|
||||
opts: {speed: 10},
|
||||
endProps: {transitionDuration: 'auto'},
|
||||
expected: 879.0953
|
||||
},
|
||||
{
|
||||
title: 'high curve',
|
||||
opts: {curve: 8},
|
||||
endProps: {transitionDuration: 'auto'},
|
||||
expected: 2016.924
|
||||
}
|
||||
];
|
||||
|
||||
test('ViewportFlyToInterpolator#initializeProps', t => {
|
||||
const interpolator = new ViewportFlyToInterpolator();
|
||||
|
||||
TEST_CASES.forEach(testCase => {
|
||||
@ -152,7 +180,7 @@ test('LinearInterpolator#initializeProps', t => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('LinearInterpolator#interpolateProps', t => {
|
||||
test('ViewportFlyToInterpolator#interpolateProps', t => {
|
||||
const interpolator = new ViewportFlyToInterpolator();
|
||||
|
||||
TEST_CASES.filter(testCase => testCase.transition).forEach(testCase => {
|
||||
@ -168,3 +196,18 @@ test('LinearInterpolator#interpolateProps', t => {
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('ViewportFlyToInterpolator#getDuration', t => {
|
||||
DURATION_TEST_CASES.forEach(testCase => {
|
||||
const interpolator = new ViewportFlyToInterpolator(testCase.opts);
|
||||
t.equal(
|
||||
toLowPrecision(
|
||||
interpolator.getDuration(START_PROPS, Object.assign({}, END_PROPS, testCase.endProps)),
|
||||
7
|
||||
),
|
||||
testCase.expected,
|
||||
`${testCase.title}: should receive correct duration`
|
||||
);
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
@ -9392,10 +9392,10 @@ verror@1.10.0:
|
||||
core-util-is "1.0.2"
|
||||
extsprintf "^1.2.0"
|
||||
|
||||
viewport-mercator-project@^6.1.0:
|
||||
version "6.1.1"
|
||||
resolved "https://registry.yarnpkg.com/viewport-mercator-project/-/viewport-mercator-project-6.1.1.tgz#d7b2cb3cb772b819f1daab17cf4019102a9102a6"
|
||||
integrity sha512-nI0GEmXnESwZxWSJuaQkdCnvOv6yckUfqqFbNB8KWVbQY3eUExVM4ZziqCVVs5mNznLjDF1auj6HLW5D5DKcng==
|
||||
viewport-mercator-project@^6.2.1:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/viewport-mercator-project/-/viewport-mercator-project-6.2.1.tgz#4d4cf376bdcf027467d0417615a0257a6316e63c"
|
||||
integrity sha512-Ns0KExngwGkX/QZCAzYbqh3TTI8LKeO8pOphZN4mZmp/+wO/HqDacbztwXMdrTLy57ToolT4XrAH2NhuK7Nyfw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.0.0"
|
||||
gl-matrix "^3.0.0"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user