Skip to content

Commit

Permalink
Viewport flyTo Interpolation: add support for auto duration and other…
Browse files Browse the repository at this point in the history
… options. (#866)

* Viewport flyTo Interpoloation: add support for auto duration, curve, speed, screenSpeed and maxDuration
  • Loading branch information
1chandu authored Sep 6, 2019
1 parent 1de9826 commit 645100f
Show file tree
Hide file tree
Showing 13 changed files with 216 additions and 46 deletions.
1 change: 1 addition & 0 deletions docs/advanced/viewport-transition.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions docs/components/fly-to-interpolator.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/5.0-release/src/utils/transition/viewport-fly-to-interpolator.js)

4 changes: 2 additions & 2 deletions examples/viewport-animation/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
});
};

Expand Down
39 changes: 28 additions & 11 deletions flow-typed/npm/viewport-mercator-project_vx.x.x.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,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",
Expand Down
2 changes: 1 addition & 1 deletion src/components/interactive-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion src/utils/math-utils.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
11 changes: 9 additions & 2 deletions src/utils/transition-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,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 {
Expand Down Expand Up @@ -169,6 +172,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 = {
Expand All @@ -181,7 +188,7 @@ export default class TransitionManager {

this.state = {
// Save current transition props
duration: endProps.transitionDuration,
duration,
easing: endProps.transitionEasing,
interpolator: endProps.transitionInterpolator,
interruption: endProps.transitionInterruption,
Expand Down
11 changes: 11 additions & 0 deletions src/utils/transition/transition-interpolator.js
Original file line number Diff line number Diff line change
@@ -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> = [];
Expand Down Expand Up @@ -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;
}
}
42 changes: 40 additions & 2 deletions src/utils/transition/viewport-fly-to-interpolator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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
Expand All @@ -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 = {};
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}
}
53 changes: 49 additions & 4 deletions test/src/utils/transition-manager.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const TEST_CASES = [
zoom: 12,
pitch: 0,
bearing: 0,
transitionDuration: 200
transitionDuration: 'auto'
},
// transitionDuration is 0
{
Expand Down Expand Up @@ -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]
}
];

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
});
Loading

0 comments on commit 645100f

Please sign in to comment.