mirror of
https://github.com/visgl/react-map-gl.git
synced 2026-01-18 15:54:22 +00:00
336 lines
10 KiB
JavaScript
336 lines
10 KiB
JavaScript
// Copyright (c) 2015 Uber Technologies, Inc.
|
|
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
// THE SOFTWARE.
|
|
|
|
// Portions of the code below originally from:
|
|
// https://github.com/mapbox/mapbox-gl-js/blob/master/js/ui/handler/scroll_zoom.js
|
|
import {PureComponent, createElement} from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import autobind from '../utils/autobind';
|
|
import {document, window} from '../utils/globals';
|
|
|
|
function noop() {}
|
|
|
|
const ua = typeof window.navigator !== 'undefined' ?
|
|
window.navigator.userAgent.toLowerCase() : '';
|
|
const firefox = ua.indexOf('firefox') !== -1;
|
|
|
|
// Extract a position from a mouse event
|
|
function getMousePosition(el, event) {
|
|
const rect = el.getBoundingClientRect();
|
|
event = event.touches ? event.touches[0] : event;
|
|
return [
|
|
event.clientX - rect.left - el.clientLeft,
|
|
event.clientY - rect.top - el.clientTop
|
|
];
|
|
}
|
|
|
|
// Extract an array of touch positions from a touch event
|
|
function getTouchPositions(el, event) {
|
|
const points = [];
|
|
const rect = el.getBoundingClientRect();
|
|
const touches = getTouches(event);
|
|
for (let i = 0; i < touches.length; i++) {
|
|
points.push([
|
|
touches[i].clientX - rect.left - el.clientLeft,
|
|
touches[i].clientY - rect.top - el.clientTop
|
|
]);
|
|
}
|
|
return points;
|
|
}
|
|
|
|
// Get relevant touches from event depending on event type (for `touchend` and
|
|
// `touchcancel` the property `changedTouches` contains the relevant coordinates)
|
|
function getTouches(event) {
|
|
const type = event.type;
|
|
if (type === 'touchend' || type === 'touchcancel') {
|
|
return event.changedTouches;
|
|
}
|
|
return event.touches;
|
|
}
|
|
|
|
// Return the centroid of an array of points
|
|
function centroid(positions) {
|
|
const sum = positions.reduce(
|
|
(acc, elt) => [acc[0] + elt[0], acc[1] + elt[1]],
|
|
[0, 0]
|
|
);
|
|
return [sum[0] / positions.length, sum[1] / positions.length];
|
|
}
|
|
|
|
const propTypes = {
|
|
width: PropTypes.number.isRequired,
|
|
height: PropTypes.number.isRequired,
|
|
pressKeyToRotate: PropTypes.bool, // True means key must be pressed to rotate instead of pan
|
|
// false to means key must be pressed to pan
|
|
onMouseDown: PropTypes.func,
|
|
onMouseDrag: PropTypes.func,
|
|
onMouseRotate: PropTypes.func,
|
|
onMouseUp: PropTypes.func,
|
|
onMouseMove: PropTypes.func,
|
|
onMouseClick: PropTypes.func,
|
|
onTouchStart: PropTypes.func,
|
|
onTouchDrag: PropTypes.func,
|
|
onTouchRotate: PropTypes.func,
|
|
onTouchEnd: PropTypes.func,
|
|
onTouchTap: PropTypes.func,
|
|
onZoom: PropTypes.func,
|
|
onZoomEnd: PropTypes.func
|
|
};
|
|
|
|
const defaultProps = {
|
|
pressKeyToRotate: true,
|
|
onMouseDown: noop,
|
|
onMouseDrag: noop,
|
|
onMouseRotate: noop,
|
|
onMouseUp: noop,
|
|
onMouseMove: noop,
|
|
onMouseClick: noop,
|
|
onTouchStart: noop,
|
|
onTouchDrag: noop,
|
|
onTouchRotate: noop,
|
|
onTouchEnd: noop,
|
|
onTouchTap: noop,
|
|
onZoom: noop,
|
|
onZoomEnd: noop
|
|
};
|
|
|
|
export default class MapInteractions extends PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
didDrag: false,
|
|
isFunctionKeyPressed: false,
|
|
startPos: null,
|
|
pos: null,
|
|
mouseWheelPos: null
|
|
};
|
|
autobind(this);
|
|
}
|
|
|
|
_getMousePos(event) {
|
|
const el = this.refs.interactionElement;
|
|
return getMousePosition(el, event);
|
|
}
|
|
|
|
_getTouchPos(event) {
|
|
const el = this.refs.interactionElement;
|
|
const positions = getTouchPositions(el, event);
|
|
return centroid(positions);
|
|
}
|
|
|
|
_isFunctionKeyPressed(event) {
|
|
return Boolean(event.metaKey || event.altKey ||
|
|
event.ctrlKey || event.shiftKey);
|
|
}
|
|
|
|
_onMouseDown(event) {
|
|
const pos = this._getMousePos(event);
|
|
this.setState({
|
|
didDrag: false,
|
|
startPos: pos,
|
|
pos,
|
|
isFunctionKeyPressed: this._isFunctionKeyPressed(event)
|
|
});
|
|
this.props.onMouseDown({pos});
|
|
document.addEventListener('mousemove', this._onMouseDrag, false);
|
|
document.addEventListener('mouseup', this._onMouseUp, false);
|
|
}
|
|
|
|
_onTouchStart(event) {
|
|
const pos = this._getTouchPos(event);
|
|
this.setState({
|
|
didDrag: false,
|
|
startPos: pos,
|
|
pos,
|
|
isFunctionKeyPressed: this._isFunctionKeyPressed(event)
|
|
});
|
|
this.props.onTouchStart({pos});
|
|
document.addEventListener('touchmove', this._onTouchDrag, false);
|
|
document.addEventListener('touchend', this._onTouchEnd, false);
|
|
}
|
|
|
|
_onMouseDrag(event) {
|
|
const pos = this._getMousePos(event);
|
|
this.setState({pos, didDrag: true});
|
|
|
|
const {isFunctionKeyPressed} = this.state;
|
|
const rotate = this.props.pressKeyToRotate ? isFunctionKeyPressed : !isFunctionKeyPressed;
|
|
if (rotate) {
|
|
const {startPos} = this.state;
|
|
this.props.onMouseRotate({pos, startPos});
|
|
} else {
|
|
this.props.onMouseDrag({pos});
|
|
}
|
|
}
|
|
|
|
_onTouchDrag(event) {
|
|
const pos = this._getTouchPos(event);
|
|
this.setState({pos, didDrag: true});
|
|
|
|
const {isFunctionKeyPressed} = this.state;
|
|
const rotate = this.props.pressKeyToRotate ? isFunctionKeyPressed : !isFunctionKeyPressed;
|
|
if (rotate) {
|
|
const {startPos} = this.state;
|
|
this.props.onTouchRotate({pos, startPos});
|
|
} else {
|
|
this.props.onTouchDrag({pos});
|
|
}
|
|
event.preventDefault();
|
|
}
|
|
|
|
_onMouseUp(event) {
|
|
document.removeEventListener('mousemove', this._onMouseDrag, false);
|
|
document.removeEventListener('mouseup', this._onMouseUp, false);
|
|
const pos = this._getMousePos(event);
|
|
this.setState({pos});
|
|
this.props.onMouseUp({pos});
|
|
if (!this.state.didDrag) {
|
|
this.props.onMouseClick({pos});
|
|
}
|
|
}
|
|
|
|
_onTouchEnd(event) {
|
|
document.removeEventListener('touchmove', this._onTouchDrag, false);
|
|
document.removeEventListener('touchend', this._onTouchEnd, false);
|
|
const pos = this._getTouchPos(event);
|
|
this.setState({pos});
|
|
this.props.onTouchEnd({pos});
|
|
if (!this.state.didDrag) {
|
|
this.props.onTouchTap({pos});
|
|
}
|
|
}
|
|
|
|
_onMouseMove(event) {
|
|
const pos = this._getMousePos(event);
|
|
this.props.onMouseMove({pos});
|
|
}
|
|
|
|
/* eslint-disable complexity, max-statements */
|
|
_onWheel(event) {
|
|
event.preventDefault();
|
|
let value = event.deltaY;
|
|
// Firefox doubles the values on retina screens...
|
|
if (firefox && event.deltaMode === window.WheelEvent.DOM_DELTA_PIXEL) {
|
|
value /= window.devicePixelRatio;
|
|
}
|
|
if (event.deltaMode === window.WheelEvent.DOM_DELTA_LINE) {
|
|
value *= 40;
|
|
}
|
|
|
|
let type = this.state.mouseWheelType;
|
|
let timeout = this.state.mouseWheelTimeout;
|
|
let lastValue = this.state.mouseWheelLastValue;
|
|
let time = this.state.mouseWheelTime;
|
|
|
|
const now = (window.performance || Date).now();
|
|
const timeDelta = now - (time || 0);
|
|
|
|
const pos = this._getMousePos(event);
|
|
time = now;
|
|
|
|
if (value !== 0 && value % 4.000244140625 === 0) {
|
|
// This one is definitely a mouse wheel event.
|
|
type = 'wheel';
|
|
// Normalize this value to match trackpad.
|
|
value = Math.floor(value / 4);
|
|
} else if (value !== 0 && Math.abs(value) < 4) {
|
|
// This one is definitely a trackpad event because it is so small.
|
|
type = 'trackpad';
|
|
} else if (timeDelta > 400) {
|
|
// This is likely a new scroll action.
|
|
type = null;
|
|
lastValue = value;
|
|
|
|
// Start a timeout in case this was a singular event, and delay it by up
|
|
// to 40ms.
|
|
timeout = window.setTimeout(function setTimeout() {
|
|
const _type = 'wheel';
|
|
this._zoom(-this.state.mouseWheelLastValue, this.state.mouseWheelPos);
|
|
this.setState({mouseWheelType: _type});
|
|
}.bind(this), 40);
|
|
} else if (!this._type) {
|
|
// This is a repeating event, but we don't know the type of event just
|
|
// yet.
|
|
// If the delta per time is small, we assume it's a fast trackpad;
|
|
// otherwise we switch into wheel mode.
|
|
type = Math.abs(timeDelta * value) < 200 ? 'trackpad' : 'wheel';
|
|
|
|
// Make sure our delayed event isn't fired again, because we accumulate
|
|
// the previous event (which was less than 40ms ago) into this event.
|
|
if (timeout) {
|
|
window.clearTimeout(timeout);
|
|
timeout = null;
|
|
value += lastValue;
|
|
}
|
|
}
|
|
|
|
// Slow down zoom if shift key is held for more precise zooming
|
|
if (event.shiftKey && value) {
|
|
value = value / 4;
|
|
}
|
|
|
|
// Only fire the callback if we actually know what type of scrolling device
|
|
// the user uses.
|
|
if (type) {
|
|
this._zoom(-value, pos);
|
|
}
|
|
|
|
this.setState({
|
|
mouseWheelTime: time,
|
|
mouseWheelPos: pos,
|
|
mouseWheelType: type,
|
|
mouseWheelTimeout: timeout,
|
|
mouseWheelLastValue: lastValue
|
|
});
|
|
}
|
|
/* eslint-enable complexity, max-statements */
|
|
|
|
_zoom(delta, pos) {
|
|
// Scale by sigmoid of scroll wheel delta.
|
|
let scale = 2 / (1 + Math.exp(-Math.abs(delta / 100)));
|
|
if (delta < 0 && scale !== 0) {
|
|
scale = 1 / scale;
|
|
}
|
|
this.props.onZoom({pos, scale});
|
|
window.clearTimeout(this._zoomEndTimeout);
|
|
this._zoomEndTimeout = window.setTimeout(function _setTimeout() {
|
|
this.props.onZoomEnd();
|
|
}.bind(this), 200);
|
|
}
|
|
|
|
render() {
|
|
const {width, height} = this.props;
|
|
|
|
return createElement('div', {
|
|
ref: 'interactionElement',
|
|
onMouseMove: this._onMouseMove,
|
|
onMouseDown: this._onMouseDown,
|
|
onTouchStart: this._onTouchStart,
|
|
onContextMenu: this._onMouseDown,
|
|
onWheel: this._onWheel,
|
|
style: {width, height, position: 'relative'}
|
|
}, this.props.children);
|
|
}
|
|
}
|
|
|
|
MapInteractions.displayName = 'MapInteractions';
|
|
MapInteractions.propTypes = propTypes;
|
|
MapInteractions.defaultProps = defaultProps;
|