package.src.ui.handler.touch_zoom_rotate.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mapbox-gl Show documentation
Show all versions of mapbox-gl Show documentation
A WebGL interactive maps library
The newest version!
// @flow
import Point from '@mapbox/point-geometry';
import DOM from '../../util/dom';
class TwoTouchHandler {
_enabled: boolean;
_active: boolean;
_firstTwoTouches: [number, number];
_vector: Point;
_startVector: Point;
_aroundCenter: boolean;
constructor() {
this.reset();
}
reset() {
this._active = false;
delete this._firstTwoTouches;
}
_start(points: [Point, Point]) {} //eslint-disable-line
_move(points: [Point, Point], pinchAround: Point, e: TouchEvent) { return {}; } //eslint-disable-line
touchstart(e: TouchEvent, points: Array, mapTouches: Array) {
//console.log(e.target, e.targetTouches.length ? e.targetTouches[0].target : null);
//log('touchstart', points, e.target.innerHTML, e.targetTouches.length ? e.targetTouches[0].target.innerHTML: undefined);
if (this._firstTwoTouches || mapTouches.length < 2) return;
this._firstTwoTouches = [
mapTouches[0].identifier,
mapTouches[1].identifier
];
// implemented by child classes
this._start([points[0], points[1]]);
}
touchmove(e: TouchEvent, points: Array, mapTouches: Array) {
if (!this._firstTwoTouches) return;
e.preventDefault();
const [idA, idB] = this._firstTwoTouches;
const a = getTouchById(mapTouches, points, idA);
const b = getTouchById(mapTouches, points, idB);
if (!a || !b) return;
const pinchAround = this._aroundCenter ? null : a.add(b).div(2);
// implemented by child classes
return this._move([a, b], pinchAround, e);
}
touchend(e: TouchEvent, points: Array, mapTouches: Array) {
if (!this._firstTwoTouches) return;
const [idA, idB] = this._firstTwoTouches;
const a = getTouchById(mapTouches, points, idA);
const b = getTouchById(mapTouches, points, idB);
if (a && b) return;
if (this._active) DOM.suppressClick();
this.reset();
}
touchcancel() {
this.reset();
}
enable(options: ?{around?: 'center'}) {
this._enabled = true;
this._aroundCenter = !!options && options.around === 'center';
}
disable() {
this._enabled = false;
this.reset();
}
isEnabled() {
return this._enabled;
}
isActive() {
return this._active;
}
}
function getTouchById(mapTouches: Array, points: Array, identifier: number) {
for (let i = 0; i < mapTouches.length; i++) {
if (mapTouches[i].identifier === identifier) return points[i];
}
}
/* ZOOM */
const ZOOM_THRESHOLD = 0.1;
function getZoomDelta(distance, lastDistance) {
return Math.log(distance / lastDistance) / Math.LN2;
}
export class TouchZoomHandler extends TwoTouchHandler {
_distance: number;
_startDistance: number;
reset() {
super.reset();
delete this._distance;
delete this._startDistance;
}
_start(points: [Point, Point]) {
this._startDistance = this._distance = points[0].dist(points[1]);
}
_move(points: [Point, Point], pinchAround: Point) {
const lastDistance = this._distance;
this._distance = points[0].dist(points[1]);
if (!this._active && Math.abs(getZoomDelta(this._distance, this._startDistance)) < ZOOM_THRESHOLD) return;
this._active = true;
return {
zoomDelta: getZoomDelta(this._distance, lastDistance),
pinchAround
};
}
}
/* ROTATE */
const ROTATION_THRESHOLD = 25; // pixels along circumference of touch circle
function getBearingDelta(a, b) {
return a.angleWith(b) * 180 / Math.PI;
}
export class TouchRotateHandler extends TwoTouchHandler {
_minDiameter: number;
reset() {
super.reset();
delete this._minDiameter;
delete this._startVector;
delete this._vector;
}
_start(points: [Point, Point]) {
this._startVector = this._vector = points[0].sub(points[1]);
this._minDiameter = points[0].dist(points[1]);
}
_move(points: [Point, Point], pinchAround: Point) {
const lastVector = this._vector;
this._vector = points[0].sub(points[1]);
if (!this._active && this._isBelowThreshold(this._vector)) return;
this._active = true;
return {
bearingDelta: getBearingDelta(this._vector, lastVector),
pinchAround
};
}
_isBelowThreshold(vector: Point) {
/*
* The threshold before a rotation actually happens is configured in
* pixels alongth circumference of the circle formed by the two fingers.
* This makes the threshold in degrees larger when the fingers are close
* together and smaller when the fingers are far apart.
*
* Use the smallest diameter from the whole gesture to reduce sensitivity
* when pinching in and out.
*/
this._minDiameter = Math.min(this._minDiameter, vector.mag());
const circumference = Math.PI * this._minDiameter;
const threshold = ROTATION_THRESHOLD / circumference * 360;
const bearingDeltaSinceStart = getBearingDelta(vector, this._startVector);
return Math.abs(bearingDeltaSinceStart) < threshold;
}
}
/* PITCH */
function isVertical(vector) {
return Math.abs(vector.y) > Math.abs(vector.x);
}
const ALLOWED_SINGLE_TOUCH_TIME = 100;
/**
* The `TouchPitchHandler` allows the user to pitch the map by dragging up and down with two fingers.
*/
export class TouchPitchHandler extends TwoTouchHandler {
_valid: boolean | void;
_firstMove: number;
_lastPoints: [Point, Point];
reset() {
super.reset();
this._valid = undefined;
delete this._firstMove;
delete this._lastPoints;
}
_start(points: [Point, Point]) {
this._lastPoints = points;
if (isVertical(points[0].sub(points[1]))) {
// fingers are more horizontal than vertical
this._valid = false;
}
}
_move(points: [Point, Point], center: Point, e: TouchEvent) {
const vectorA = points[0].sub(this._lastPoints[0]);
const vectorB = points[1].sub(this._lastPoints[1]);
this._valid = this.gestureBeginsVertically(vectorA, vectorB, e.timeStamp);
if (!this._valid) return;
this._lastPoints = points;
this._active = true;
const yDeltaAverage = (vectorA.y + vectorB.y) / 2;
const degreesPerPixelMoved = -0.5;
return {
pitchDelta: yDeltaAverage * degreesPerPixelMoved
};
}
gestureBeginsVertically(vectorA: Point, vectorB: Point, timeStamp: number) {
if (this._valid !== undefined) return this._valid;
const threshold = 2;
const movedA = vectorA.mag() >= threshold;
const movedB = vectorB.mag() >= threshold;
// neither finger has moved a meaningful amount, wait
if (!movedA && !movedB) return;
// One finger has moved and the other has not.
// If enough time has passed, decide it is not a pitch.
if (!movedA || !movedB) {
if (this._firstMove === undefined) {
this._firstMove = timeStamp;
}
if (timeStamp - this._firstMove < ALLOWED_SINGLE_TOUCH_TIME) {
// still waiting for a movement from the second finger
return undefined;
} else {
return false;
}
}
const isSameDirection = vectorA.y > 0 === vectorB.y > 0;
return isVertical(vectorA) && isVertical(vectorB) && isSameDirection;
}
/**
* Returns a Boolean indicating whether the "drag to pitch" interaction is enabled.
*
* @memberof TouchPitchHandler
* @name isEnabled
* @instance
* @returns {boolean} `true` if the "drag to pitch" interaction is enabled.
*/
/**
* Returns a Boolean indicating whether the "drag to pitch" interaction is active, i.e. currently being used.
*
* @memberof TouchPitchHandler
* @name isActive
* @instance
* @returns {boolean} `true` if the "drag to pitch" interaction is active.
*/
/**
* Enables the "drag to pitch" interaction.
*
* @memberof TouchPitchHandler
* @name enable
* @instance
* @example
* map.touchPitch.enable();
*/
/**
* Disables the "drag to pitch" interaction.
*
* @memberof TouchPitchHandler
* @name disable
* @instance
* @example
* map.touchPitch.disable();
*/
}