mirror of
https://github.com/visgl/react-map-gl.git
synced 2026-01-25 16:02:50 +00:00
Port TransitionManager from deck.gl (#383)
This commit is contained in:
parent
e7ff75e1e6
commit
25c3b795ba
@ -1,21 +1,13 @@
|
||||
/* global window */
|
||||
import React, {Component} from 'react';
|
||||
import {render} from 'react-dom';
|
||||
import MapGL from 'react-map-gl';
|
||||
import MapGL, {experimental} from 'react-map-gl';
|
||||
import {PerspectiveMercatorViewport} from 'viewport-mercator-project';
|
||||
import TWEEN from 'tween.js';
|
||||
|
||||
import ControlPanel from './control-panel';
|
||||
|
||||
const MAPBOX_TOKEN = ''; // Set your mapbox token here
|
||||
|
||||
// Required by tween.js
|
||||
function animate() {
|
||||
TWEEN.update();
|
||||
window.requestAnimationFrame(animate);
|
||||
}
|
||||
animate();
|
||||
|
||||
export default class App extends Component {
|
||||
|
||||
state = {
|
||||
@ -39,34 +31,25 @@ export default class App extends Component {
|
||||
window.removeEventListener('resize', this._resize);
|
||||
}
|
||||
|
||||
_resize = () => {
|
||||
this.setState({
|
||||
viewport: {
|
||||
...this.state.viewport,
|
||||
width: this.props.width || window.innerWidth,
|
||||
height: this.props.height || window.innerHeight
|
||||
}
|
||||
_onViewportChange = viewport => this.setState({
|
||||
viewport: {...this.state.viewport, ...viewport}
|
||||
});
|
||||
|
||||
_resize = () => this._onViewportChange({
|
||||
width: this.props.width || window.innerWidth,
|
||||
height: this.props.height || window.innerHeight
|
||||
});
|
||||
|
||||
_goToViewport = ({longitude, latitude}) => {
|
||||
this._onViewportChange({
|
||||
longitude,
|
||||
latitude,
|
||||
zoom: 11,
|
||||
transitionInterpolator: experimental.viewportFlyToInterpolator,
|
||||
transitionDuration: 3000
|
||||
});
|
||||
};
|
||||
|
||||
_easeTo = ({longitude, latitude}) => {
|
||||
// Remove existing animations
|
||||
TWEEN.removeAll();
|
||||
|
||||
const {viewport} = this.state;
|
||||
|
||||
new TWEEN.Tween(viewport)
|
||||
.to({
|
||||
longitude, latitude,
|
||||
zoom: 11
|
||||
}, 3000)
|
||||
.easing(TWEEN.Easing.Cubic.InOut)
|
||||
.onUpdate(() => this._onViewportChange(viewport))
|
||||
.start();
|
||||
};
|
||||
|
||||
_onViewportChange = viewport => this.setState({viewport});
|
||||
|
||||
render() {
|
||||
|
||||
const {viewport, settings} = this.state;
|
||||
@ -81,7 +64,7 @@ export default class App extends Component {
|
||||
dragToRotate={false}
|
||||
mapboxApiAccessToken={MAPBOX_TOKEN} />
|
||||
<ControlPanel containerComponent={this.props.containerComponent}
|
||||
onViewportChange={this._easeTo} />
|
||||
onViewportChange={this._goToViewport} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -38,9 +38,10 @@
|
||||
"bowser": "^1.2.0",
|
||||
"immutable": "*",
|
||||
"mapbox-gl": "0.40.1",
|
||||
"math.gl": ">= 1.0.0-alpha.8",
|
||||
"mjolnir.js": ">=0.4.0",
|
||||
"prop-types": "^15.5.7",
|
||||
"viewport-mercator-project": "^4.0.1"
|
||||
"viewport-mercator-project": ">= 4.2.0-alpha.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.22.2",
|
||||
|
||||
@ -6,6 +6,8 @@ import StaticMap from './static-map';
|
||||
import {MAPBOX_LIMITS} from '../utils/map-state';
|
||||
import {PerspectiveMercatorViewport} from 'viewport-mercator-project';
|
||||
|
||||
import TransitionManager from '../utils/transition-manager';
|
||||
|
||||
import {EventManager} from 'mjolnir.js';
|
||||
import MapControls from '../utils/map-controls';
|
||||
import config from '../config';
|
||||
@ -31,6 +33,20 @@ const propTypes = Object.assign({}, StaticMap.propTypes, {
|
||||
*/
|
||||
onViewportChange: PropTypes.func,
|
||||
|
||||
/** Viewport transition **/
|
||||
// transition duration for viewport change
|
||||
transitionDuration: PropTypes.number,
|
||||
// function called for each transition step, can be used to perform custom transitions.
|
||||
transitionInterpolator: PropTypes.func,
|
||||
// type of interruption of current transition on update.
|
||||
transitionInterruption: PropTypes.number,
|
||||
// easing function
|
||||
transitionEasing: PropTypes.func,
|
||||
// transition status update functions
|
||||
onTransitionStart: PropTypes.func,
|
||||
onTransitionInterrupt: PropTypes.func,
|
||||
onTransitionEnd: PropTypes.func,
|
||||
|
||||
/** Enables control event handling */
|
||||
// Scroll to zoom
|
||||
scrollZoom: PropTypes.bool,
|
||||
@ -100,22 +116,25 @@ const getDefaultCursor = ({isDragging, isHovering}) => isDragging ?
|
||||
config.CURSOR.GRABBING :
|
||||
(isHovering ? config.CURSOR.POINTER : config.CURSOR.GRAB);
|
||||
|
||||
const defaultProps = Object.assign({}, StaticMap.defaultProps, MAPBOX_LIMITS, {
|
||||
onViewportChange: null,
|
||||
onClick: null,
|
||||
onHover: null,
|
||||
const defaultProps = Object.assign({},
|
||||
StaticMap.defaultProps, MAPBOX_LIMITS, TransitionManager.defaultProps,
|
||||
{
|
||||
onViewportChange: null,
|
||||
onClick: null,
|
||||
onHover: null,
|
||||
|
||||
scrollZoom: true,
|
||||
dragPan: true,
|
||||
dragRotate: true,
|
||||
doubleClickZoom: true,
|
||||
touchZoomRotate: true,
|
||||
scrollZoom: true,
|
||||
dragPan: true,
|
||||
dragRotate: true,
|
||||
doubleClickZoom: true,
|
||||
touchZoomRotate: true,
|
||||
|
||||
clickRadius: 0,
|
||||
getCursor: getDefaultCursor,
|
||||
clickRadius: 0,
|
||||
getCursor: getDefaultCursor,
|
||||
|
||||
visibilityConstraints: MAPBOX_LIMITS
|
||||
});
|
||||
visibilityConstraints: MAPBOX_LIMITS
|
||||
}
|
||||
);
|
||||
|
||||
const childContextTypes = {
|
||||
viewport: PropTypes.instanceOf(PerspectiveMercatorViewport),
|
||||
@ -167,10 +186,13 @@ export default class InteractiveMap extends PureComponent {
|
||||
onStateChange: this._onInteractiveStateChange,
|
||||
eventManager
|
||||
}));
|
||||
|
||||
this._transitionManager = new TransitionManager(this.props);
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
this._mapControls.setOptions(nextProps);
|
||||
this._transitionManager.processViewportChange(nextProps);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -189,7 +211,7 @@ export default class InteractiveMap extends PureComponent {
|
||||
}
|
||||
|
||||
// Checks a visibilityConstraints object to see if the map should be displayed
|
||||
checkVisibilityConstraints(props) {
|
||||
_checkVisibilityConstraints(props) {
|
||||
const capitalize = s => s[0].toUpperCase() + s.slice(1);
|
||||
|
||||
const {visibilityConstraints} = props;
|
||||
@ -290,11 +312,14 @@ export default class InteractiveMap extends PureComponent {
|
||||
ref: this._eventCanvasLoaded,
|
||||
style: eventCanvasStyle
|
||||
},
|
||||
createElement(StaticMap, Object.assign({}, this.props, {
|
||||
visible: this.checkVisibilityConstraints(this.props),
|
||||
ref: this._staticMapLoaded,
|
||||
children: this._eventManager ? this.props.children : null
|
||||
}))
|
||||
createElement(StaticMap, Object.assign({}, this.props,
|
||||
this._transitionManager && this._transitionManager.getViewportInTransition(),
|
||||
{
|
||||
visible: this._checkVisibilityConstraints(this.props),
|
||||
ref: this._staticMapLoaded,
|
||||
children: this._eventManager ? this.props.children : null
|
||||
}
|
||||
))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -4,9 +4,14 @@ import BaseControl from './base-control';
|
||||
import autobind from '../utils/autobind';
|
||||
|
||||
import MapState from '../utils/map-state';
|
||||
import TransitionManager from '../utils/transition-manager';
|
||||
|
||||
import deprecateWarn from '../utils/deprecate-warn';
|
||||
|
||||
const LINEAR_TRANSITION_PROPS = Object.assign({}, TransitionManager.defaultProps, {
|
||||
transitionDuration: 300
|
||||
});
|
||||
|
||||
const propTypes = Object.assign({}, BaseControl.propTypes, {
|
||||
// Custom className
|
||||
className: PropTypes.string,
|
||||
@ -45,7 +50,9 @@ export default class NavigationControl extends BaseControl {
|
||||
const mapState = new MapState(Object.assign({}, viewport, opts));
|
||||
// TODO(deprecate): remove this check when `onChangeViewport` gets deprecated
|
||||
const onViewportChange = this.props.onChangeViewport || this.props.onViewportChange;
|
||||
onViewportChange(mapState.getViewportProps());
|
||||
const newViewport = Object.assign({}, mapState.getViewportProps(), LINEAR_TRANSITION_PROPS);
|
||||
|
||||
onViewportChange(newViewport);
|
||||
}
|
||||
|
||||
_onZoomIn() {
|
||||
@ -57,7 +64,7 @@ export default class NavigationControl extends BaseControl {
|
||||
}
|
||||
|
||||
_onResetNorth() {
|
||||
this._updateViewport({bearing: 0});
|
||||
this._updateViewport({bearing: 0, pitch: 0});
|
||||
}
|
||||
|
||||
_renderCompass() {
|
||||
|
||||
@ -34,6 +34,10 @@ export {default as CanvasOverlay} from './overlays/canvas-overlay';
|
||||
export {default as HTMLOverlay} from './overlays/html-overlay';
|
||||
export {default as SVGOverlay} from './overlays/svg-overlay';
|
||||
|
||||
import {TRANSITION_EVENTS} from './utils/transition-manager';
|
||||
import {viewportLinearInterpolator, viewportFlyToInterpolator}
|
||||
from './utils/viewport-transition-utils';
|
||||
|
||||
// Utilities
|
||||
|
||||
// Experimental Features (May change in minor version bumps, use at your own risk)
|
||||
@ -42,5 +46,8 @@ import autobind from './utils/autobind';
|
||||
|
||||
export const experimental = {
|
||||
MapControls,
|
||||
autobind
|
||||
autobind,
|
||||
TRANSITION_EVENTS,
|
||||
viewportLinearInterpolator,
|
||||
viewportFlyToInterpolator
|
||||
};
|
||||
|
||||
@ -18,7 +18,15 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import MapState from '../utils/map-state';
|
||||
import MapState from './map-state';
|
||||
import TransitionManager from './transition-manager';
|
||||
|
||||
const NO_TRANSITION_PROPS = {
|
||||
transitionDuration: 0
|
||||
};
|
||||
const LINEAR_TRANSITION_PROPS = Object.assign({}, TransitionManager.defaultProps, {
|
||||
transitionDuration: 300
|
||||
});
|
||||
|
||||
// EVENT HANDLING PARAMETERS
|
||||
const PITCH_MOUSE_THRESHOLD = 5;
|
||||
@ -99,9 +107,9 @@ export default class MapControls {
|
||||
|
||||
/* Callback util */
|
||||
// formats map state and invokes callback function
|
||||
updateViewport(newMapState, extraState = {}) {
|
||||
updateViewport(newMapState, extraProps = {}, extraState = {}) {
|
||||
const oldViewport = this.mapState.getViewportProps();
|
||||
const newViewport = newMapState.getViewportProps();
|
||||
const newViewport = Object.assign({}, newMapState.getViewportProps(), extraProps);
|
||||
|
||||
if (this.onViewportChange &&
|
||||
Object.keys(newViewport).some(key => oldViewport[key] !== newViewport[key])) {
|
||||
@ -182,7 +190,7 @@ export default class MapControls {
|
||||
_onPanStart(event) {
|
||||
const pos = this.getCenter(event);
|
||||
const newMapState = this.mapState.panStart({pos}).rotateStart({pos});
|
||||
return this.updateViewport(newMapState, {isDragging: true});
|
||||
return this.updateViewport(newMapState, NO_TRANSITION_PROPS, {isDragging: true});
|
||||
}
|
||||
|
||||
// Default handler for the `panmove` event.
|
||||
@ -194,7 +202,7 @@ export default class MapControls {
|
||||
// Default handler for the `panend` event.
|
||||
_onPanEnd(event) {
|
||||
const newMapState = this.mapState.panEnd().rotateEnd();
|
||||
return this.updateViewport(newMapState, {isDragging: false});
|
||||
return this.updateViewport(newMapState, null, {isDragging: false});
|
||||
}
|
||||
|
||||
// Default handler for panning to move.
|
||||
@ -205,7 +213,7 @@ export default class MapControls {
|
||||
}
|
||||
const pos = this.getCenter(event);
|
||||
const newMapState = this.mapState.pan({pos});
|
||||
return this.updateViewport(newMapState);
|
||||
return this.updateViewport(newMapState, NO_TRANSITION_PROPS, {isDragging: true});
|
||||
}
|
||||
|
||||
// Default handler for panning to rotate.
|
||||
@ -237,7 +245,7 @@ export default class MapControls {
|
||||
deltaScaleY = Math.min(1, Math.max(-1, deltaScaleY));
|
||||
|
||||
const newMapState = this.mapState.rotate({deltaScaleX, deltaScaleY});
|
||||
return this.updateViewport(newMapState);
|
||||
return this.updateViewport(newMapState, NO_TRANSITION_PROPS, {isDragging: true});
|
||||
}
|
||||
|
||||
// Default handler for the `wheel` event.
|
||||
@ -256,14 +264,14 @@ export default class MapControls {
|
||||
}
|
||||
|
||||
const newMapState = this.mapState.zoom({pos, scale});
|
||||
return this.updateViewport(newMapState);
|
||||
return this.updateViewport(newMapState, NO_TRANSITION_PROPS);
|
||||
}
|
||||
|
||||
// Default handler for the `pinchstart` event.
|
||||
_onPinchStart(event) {
|
||||
const pos = this.getCenter(event);
|
||||
const newMapState = this.mapState.zoomStart({pos});
|
||||
return this.updateViewport(newMapState, {isDragging: true});
|
||||
return this.updateViewport(newMapState, NO_TRANSITION_PROPS, {isDragging: true});
|
||||
}
|
||||
|
||||
// Default handler for the `pinch` event.
|
||||
@ -274,13 +282,13 @@ export default class MapControls {
|
||||
const pos = this.getCenter(event);
|
||||
const {scale} = event;
|
||||
const newMapState = this.mapState.zoom({pos, scale});
|
||||
return this.updateViewport(newMapState);
|
||||
return this.updateViewport(newMapState, NO_TRANSITION_PROPS, {isDragging: true});
|
||||
}
|
||||
|
||||
// Default handler for the `pinchend` event.
|
||||
_onPinchEnd(event) {
|
||||
const newMapState = this.mapState.zoomEnd();
|
||||
return this.updateViewport(newMapState, {isDragging: false});
|
||||
return this.updateViewport(newMapState, null, {isDragging: false});
|
||||
}
|
||||
|
||||
// Default handler for the `doubletap` event.
|
||||
@ -292,7 +300,7 @@ export default class MapControls {
|
||||
const isZoomOut = this.isFunctionKeyPressed(event);
|
||||
|
||||
const newMapState = this.mapState.zoom({pos, scale: isZoomOut ? 0.5 : 2});
|
||||
return this.updateViewport(newMapState);
|
||||
return this.updateViewport(newMapState, LINEAR_TRANSITION_PROPS);
|
||||
}
|
||||
|
||||
/* eslint-disable complexity */
|
||||
@ -351,7 +359,7 @@ export default class MapControls {
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return this.updateViewport(newMapState);
|
||||
return this.updateViewport(newMapState, LINEAR_TRANSITION_PROPS);
|
||||
}
|
||||
/* eslint-enable complexity */
|
||||
}
|
||||
|
||||
@ -278,10 +278,16 @@ export default class MapState {
|
||||
}
|
||||
|
||||
// Apply any constraints (mathematical or defined by _viewportProps) to map state
|
||||
/* eslint-disable complexity */
|
||||
_applyConstraints(props) {
|
||||
// Normalize degrees
|
||||
props.longitude = mod(props.longitude + 180, 360) - 180;
|
||||
props.bearing = mod(props.bearing + 180, 360) - 180;
|
||||
const {longitude, bearing} = props;
|
||||
if (longitude < -180 || longitude > 180) {
|
||||
props.longitude = mod(longitude + 180, 360) - 180;
|
||||
}
|
||||
if (bearing < -180 || bearing > 180) {
|
||||
props.bearing = mod(bearing + 180, 360) - 180;
|
||||
}
|
||||
|
||||
// Ensure zoom is within specified range
|
||||
const {maxZoom, minZoom, zoom} = props;
|
||||
@ -319,6 +325,7 @@ export default class MapState {
|
||||
|
||||
return props;
|
||||
}
|
||||
/* eslint-enable complexity */
|
||||
|
||||
// Returns {viewport, latitudeRange: [topY, bottomY]} in non-perspective mode
|
||||
_getLatitudeRange(props) {
|
||||
|
||||
181
src/utils/transition-manager.js
Normal file
181
src/utils/transition-manager.js
Normal file
@ -0,0 +1,181 @@
|
||||
/* global requestAnimationFrame, cancelAnimationFrame */
|
||||
import assert from 'assert';
|
||||
import {
|
||||
viewportLinearInterpolator,
|
||||
extractViewportFrom,
|
||||
areViewportsEqual
|
||||
} from './viewport-transition-utils';
|
||||
import MapState from './map-state';
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
export const TRANSITION_EVENTS = {
|
||||
BREAK: 1,
|
||||
SNAP_TO_END: 2,
|
||||
IGNORE: 3
|
||||
};
|
||||
|
||||
const DEFAULT_PROPS = {
|
||||
transitionDuration: 0,
|
||||
transitionInterpolator: viewportLinearInterpolator,
|
||||
transitionEasing: t => t,
|
||||
transitionInterruption: TRANSITION_EVENTS.BREAK,
|
||||
onTransitionStart: noop,
|
||||
onTransitionInterrupt: noop,
|
||||
onTransitionEnd: noop
|
||||
};
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
animation: null,
|
||||
viewport: null,
|
||||
startViewport: null,
|
||||
endViewport: null
|
||||
};
|
||||
|
||||
export default class TransitionManager {
|
||||
constructor(props) {
|
||||
this.props = props;
|
||||
this.state = DEFAULT_STATE;
|
||||
|
||||
this._onTransitionFrame = this._onTransitionFrame.bind(this);
|
||||
}
|
||||
|
||||
// Returns current transitioned viewport.
|
||||
getViewportInTransition() {
|
||||
return this.state.viewport;
|
||||
}
|
||||
|
||||
// Process the vewiport change, either ignore or trigger a new transiton.
|
||||
processViewportChange(nextProps) {
|
||||
|
||||
// NOTE: Be cautious re-ordering statements in this function.
|
||||
if (this._shouldIgnoreViewportChange(nextProps)) {
|
||||
this.props = nextProps;
|
||||
return;
|
||||
}
|
||||
|
||||
const isTransitionInProgress = this._isTransitionInProgress();
|
||||
|
||||
if (this._isTransitionEnabled(nextProps)) {
|
||||
const currentViewport = this.state.viewport || extractViewportFrom(this.props);
|
||||
const endViewport = this.state.endViewport;
|
||||
|
||||
const startViewport = this.state.interruption === TRANSITION_EVENTS.SNAP_TO_END ?
|
||||
(endViewport || currentViewport) :
|
||||
currentViewport;
|
||||
|
||||
this._triggerTransition(startViewport, nextProps);
|
||||
|
||||
if (isTransitionInProgress) {
|
||||
this.props.onTransitionInterrupt();
|
||||
}
|
||||
nextProps.onTransitionStart();
|
||||
} else if (isTransitionInProgress) {
|
||||
this.props.onTransitionInterrupt();
|
||||
this._endTransition();
|
||||
}
|
||||
|
||||
this.props = nextProps;
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
_isTransitionInProgress() {
|
||||
return this.state.viewport;
|
||||
}
|
||||
|
||||
_isTransitionEnabled(props) {
|
||||
return props.transitionDuration > 0;
|
||||
}
|
||||
|
||||
_isUpdateDueToCurrentTransition(props) {
|
||||
if (this.state.viewport) {
|
||||
return areViewportsEqual(props, this.state.viewport);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_shouldIgnoreViewportChange(nextProps) {
|
||||
// Ignore update if it is due to current active transition.
|
||||
// Ignore update if it is requested to be ignored
|
||||
if (this._isTransitionInProgress()) {
|
||||
if (this.state.interruption === TRANSITION_EVENTS.IGNORE ||
|
||||
this._isUpdateDueToCurrentTransition(nextProps)) {
|
||||
return true;
|
||||
}
|
||||
} else if (!this._isTransitionEnabled(nextProps)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ignore if none of the viewport props changed.
|
||||
if (areViewportsEqual(this.props, nextProps)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_triggerTransition(startViewport, nextProps) {
|
||||
assert(nextProps.transitionDuration !== 0);
|
||||
const endViewport = extractViewportFrom(nextProps);
|
||||
|
||||
cancelAnimationFrame(this.state.animation);
|
||||
|
||||
this.state = {
|
||||
// Save current transition props
|
||||
duration: nextProps.transitionDuration,
|
||||
easing: nextProps.transitionEasing,
|
||||
interpolator: nextProps.transitionInterpolator,
|
||||
interruption: nextProps.transitionInterruption,
|
||||
|
||||
startTime: Date.now(),
|
||||
startViewport,
|
||||
endViewport,
|
||||
animation: null,
|
||||
viewport: startViewport
|
||||
};
|
||||
|
||||
this._onTransitionFrame();
|
||||
}
|
||||
|
||||
_onTransitionFrame() {
|
||||
// _updateViewport() may cancel the animation
|
||||
this.state.animation = requestAnimationFrame(this._onTransitionFrame);
|
||||
this._updateViewport();
|
||||
}
|
||||
|
||||
_endTransition() {
|
||||
cancelAnimationFrame(this.state.animation);
|
||||
this.state = DEFAULT_STATE;
|
||||
}
|
||||
|
||||
_updateViewport() {
|
||||
// NOTE: Be cautious re-ordering statements in this function.
|
||||
const currentTime = Date.now();
|
||||
const {startTime, duration, easing, interpolator, startViewport, endViewport} = this.state;
|
||||
|
||||
let shouldEnd = false;
|
||||
let t = (currentTime - startTime) / duration;
|
||||
if (t >= 1) {
|
||||
t = 1;
|
||||
shouldEnd = true;
|
||||
}
|
||||
t = easing(t);
|
||||
|
||||
const viewport = interpolator(startViewport, endViewport, t);
|
||||
// Normalize viewport props
|
||||
const mapState = new MapState(Object.assign({}, this.props, viewport));
|
||||
this.state.viewport = mapState.getViewportProps();
|
||||
|
||||
if (this.props.onViewportChange) {
|
||||
this.props.onViewportChange(this.state.viewport);
|
||||
}
|
||||
|
||||
if (shouldEnd) {
|
||||
this._endTransition();
|
||||
this.props.onTransitionEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TransitionManager.defaultProps = DEFAULT_PROPS;
|
||||
153
src/utils/viewport-transition-utils.js
Normal file
153
src/utils/viewport-transition-utils.js
Normal file
@ -0,0 +1,153 @@
|
||||
/* eslint max-statements: ["error", 50] */
|
||||
|
||||
import {projectFlat, unprojectFlat} from 'viewport-mercator-project';
|
||||
import {Vector2} from 'math.gl';
|
||||
|
||||
const EPSILON = 0.01;
|
||||
const VIEWPORT_PROPS = ['longitude', 'latitude', 'zoom', 'bearing', 'pitch',
|
||||
'position', 'width', 'height'];
|
||||
const VIEWPORT_INTERPOLATION_PROPS =
|
||||
['longitude', 'latitude', 'zoom', 'bearing', 'pitch', 'position'];
|
||||
|
||||
/** Util functions */
|
||||
function lerp(start, end, step) {
|
||||
if (Array.isArray(start)) {
|
||||
return start.map((element, index) => {
|
||||
return lerp(element, end[index], step);
|
||||
});
|
||||
}
|
||||
return step * end + (1 - step) * start;
|
||||
}
|
||||
|
||||
function zoomToScale(zoom) {
|
||||
return Math.pow(2, zoom);
|
||||
}
|
||||
|
||||
function scaleToZoom(scale) {
|
||||
return Math.log2(scale);
|
||||
}
|
||||
|
||||
export function extractViewportFrom(props) {
|
||||
const viewport = {};
|
||||
VIEWPORT_PROPS.forEach((key) => {
|
||||
if (typeof props[key] !== 'undefined') {
|
||||
viewport[key] = props[key];
|
||||
}
|
||||
});
|
||||
return viewport;
|
||||
}
|
||||
|
||||
/* eslint-disable max-depth */
|
||||
export function areViewportsEqual(startViewport, endViewport) {
|
||||
for (const p of VIEWPORT_INTERPOLATION_PROPS) {
|
||||
if (Array.isArray(startViewport[p])) {
|
||||
for (let i = 0; i < startViewport[p].length; ++i) {
|
||||
if (startViewport[p][i] !== endViewport[p][i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (startViewport[p] !== endViewport[p]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/* eslint-enable max-depth */
|
||||
|
||||
/**
|
||||
* Performs linear interpolation of two viewports.
|
||||
* @param {Object} startViewport - object containing starting viewport parameters.
|
||||
* @param {Object} endViewport - object containing ending viewport parameters.
|
||||
* @param {Number} t - interpolation step.
|
||||
* @return {Object} - interpolated viewport for given step.
|
||||
*/
|
||||
export function viewportLinearInterpolator(startViewport, endViewport, t) {
|
||||
const viewport = {};
|
||||
|
||||
for (const p of VIEWPORT_INTERPOLATION_PROPS) {
|
||||
const startValue = startViewport[p];
|
||||
const endValue = endViewport[p];
|
||||
// TODO: 'position' is not always specified
|
||||
if (typeof startValue !== 'undefined' && typeof endValue !== 'undefined') {
|
||||
viewport[p] = lerp(startValue, endValue, t);
|
||||
}
|
||||
}
|
||||
return viewport;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method adapts mapbox-gl-js Map#flyTo animation so it can be used in
|
||||
* react/redux architecture.
|
||||
* mapbox-gl-js flyTo : https://www.mapbox.com/mapbox-gl-js/api/#map#flyto.
|
||||
* It implements “Smooth and efficient zooming and panning.” algorithm by
|
||||
* "Jarke J. van Wijk and Wim A.A. Nuij"
|
||||
*
|
||||
* @param {Object} startViewport - object containing starting viewport parameters.
|
||||
* @param {Object} endViewport - object containing ending viewport parameters.
|
||||
* @param {Number} t - interpolation step.
|
||||
* @return {Object} - interpolated viewport for given step.
|
||||
*/
|
||||
export function viewportFlyToInterpolator(startViewport, endViewport, t) {
|
||||
// Equations from above paper are referred where needed.
|
||||
|
||||
const viewport = {};
|
||||
|
||||
// TODO: add this as an option for applications.
|
||||
const rho = 1.414;
|
||||
|
||||
const startZoom = startViewport.zoom;
|
||||
const startCenter = [startViewport.longitude, startViewport.latitude];
|
||||
const startScale = zoomToScale(startZoom);
|
||||
const endZoom = endViewport.zoom;
|
||||
const endCenter = [endViewport.longitude, endViewport.latitude];
|
||||
const scale = zoomToScale(endZoom - startZoom);
|
||||
|
||||
const startCenterXY = new Vector2(projectFlat(startCenter, startScale));
|
||||
const endCenterXY = new Vector2(projectFlat(endCenter, startScale));
|
||||
const uDelta = endCenterXY.subtract(startCenterXY);
|
||||
|
||||
const w0 = Math.max(startViewport.width, startViewport.height);
|
||||
const w1 = w0 / scale;
|
||||
const u1 = Math.sqrt((uDelta.x * uDelta.x) + (uDelta.y * uDelta.y));
|
||||
// u0 is treated as '0' in Eq (9).
|
||||
|
||||
// Linearly interpolate 'bearing' and 'pitch'
|
||||
for (const p of ['bearing', 'pitch']) {
|
||||
const startValue = startViewport[p];
|
||||
const endValue = endViewport[p];
|
||||
viewport[p] = lerp(startValue, endValue, t);
|
||||
}
|
||||
|
||||
// If change in center is too small, do linear interpolaiton.
|
||||
if (Math.abs(u1) < EPSILON) {
|
||||
for (const p of ['latitude', 'longitude', 'zoom']) {
|
||||
const startValue = startViewport[p];
|
||||
const endValue = endViewport[p];
|
||||
viewport[p] = lerp(startValue, endValue, t);
|
||||
}
|
||||
return viewport;
|
||||
}
|
||||
|
||||
// Implement Equation (9) from above algorithm.
|
||||
const rho2 = rho * rho;
|
||||
const b0 = (w1 * w1 - w0 * w0 + rho2 * rho2 * u1 * u1) / (2 * w0 * rho2 * u1);
|
||||
const b1 = (w1 * w1 - w0 * w0 - rho2 * rho2 * u1 * u1) / (2 * w1 * rho2 * u1);
|
||||
const r0 = Math.log(Math.sqrt(b0 * b0 + 1) - b0);
|
||||
const r1 = Math.log(Math.sqrt(b1 * b1 + 1) - b1);
|
||||
const S = (r1 - r0) / rho;
|
||||
const s = t * S;
|
||||
|
||||
const w = (Math.cosh(r0) / Math.cosh(r0 + rho * s));
|
||||
const u = w0 * ((Math.cosh(r0) * Math.tanh(r0 + rho * s) - Math.sinh(r0)) / rho2) / u1;
|
||||
|
||||
const scaleIncrement = 1 / w; // Using w method for scaling.
|
||||
const newZoom = startZoom + scaleToZoom(scaleIncrement);
|
||||
|
||||
const newCenter = unprojectFlat(
|
||||
(startCenterXY.add(uDelta.scale(u))).scale(scaleIncrement),
|
||||
zoomToScale(newZoom));
|
||||
viewport.longitude = newCenter[0];
|
||||
viewport.latitude = newCenter[1];
|
||||
viewport.zoom = newZoom;
|
||||
return viewport;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user