maplibre-gl
Version:
BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
336 lines (272 loc) • 9.95 kB
text/typescript
import type Point from '@mapbox/point-geometry';
import {DOM} from '../../util/dom';
import type {Map} from '../map';
import {type Handler, type HandlerResult} from '../handler_manager';
/**
* An options object sent to the enable function of some of the handlers
*/
export type AroundCenterOptions = {
/**
* If "center" is passed, map will zoom around the center of map
*/
around: 'center';
};
/**
* The `TwoFingersTouchHandler`s allows the user to zoom, pitch and rotate the map using two fingers
*
*/
abstract class TwoFingersTouchHandler implements Handler {
_enabled?: boolean;
_active?: boolean;
_firstTwoTouches?: [number, number];
_vector?: Point;
_startVector?: Point;
_aroundCenter?: boolean;
/** @internal */
constructor() {
this.reset();
}
reset(): void {
this._active = false;
delete this._firstTwoTouches;
}
abstract _start(points: [Point, Point]): void;
abstract _move(points: [Point, Point], pinchAround: Point | null, e: TouchEvent): HandlerResult | void;
touchstart(e: TouchEvent, points: Array<Point>, mapTouches: Array<Touch>): void {
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<Point>, mapTouches: Array<Touch>): HandlerResult | void {
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<Point>, mapTouches: Array<Touch>): void {
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(): void {
this.reset();
}
/**
* Enables the "drag to pitch" interaction.
*
* @example
* ```ts
* map.touchPitch.enable();
* ```
*/
enable(options?: AroundCenterOptions | boolean | null): void {
this._enabled = true;
this._aroundCenter = !!options && (options as AroundCenterOptions).around === 'center';
}
/**
* Disables the "drag to pitch" interaction.
*
* @example
* ```ts
* map.touchPitch.disable();
* ```
*/
disable(): void {
this._enabled = false;
this.reset();
}
/**
* Returns a Boolean indicating whether the "drag to pitch" interaction is enabled.
*
* @returns `true` if the "drag to pitch" interaction is enabled.
*/
isEnabled(): boolean {
return !!this._enabled;
}
/**
* Returns a Boolean indicating whether the "drag to pitch" interaction is active, i.e. currently being used.
*
* @returns `true` if the "drag to pitch" interaction is active.
*/
isActive(): boolean {
return !!this._active;
}
}
function getTouchById(mapTouches: Array<Touch>, points: Array<Point>, identifier: number): Point | undefined {
for (let i = 0; i < mapTouches.length; i++) {
if (mapTouches[i].identifier === identifier) return points[i];
}
return undefined;
}
/* ZOOM */
const ZOOM_THRESHOLD = 0.1;
function getZoomDelta(distance: number, lastDistance: number): number {
return Math.log(distance / lastDistance) / Math.LN2;
}
/**
* The `TwoFingersTouchHandler`s allows the user to zoom the map two fingers
*
* @group Handlers
*/
export class TwoFingersTouchZoomHandler extends TwoFingersTouchHandler {
_distance?: number;
_startDistance?: number;
reset() {
super.reset();
delete this._distance;
delete this._startDistance;
}
_start(points: [Point, Point]): void {
this._startDistance = this._distance = points[0].dist(points[1]);
}
_move(points: [Point, Point], pinchAround: Point | null): HandlerResult | void {
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: Point, b: Point): number {
return a.angleWith(b) * 180 / Math.PI;
}
/**
* The `TwoFingersTouchHandler`s allows the user to rotate the map two fingers
*
* @group Handlers
*/
export class TwoFingersTouchRotateHandler extends TwoFingersTouchHandler {
_minDiameter?: number;
reset(): void {
super.reset();
delete this._minDiameter;
delete this._startVector;
delete this._vector;
}
_start(points: [Point, Point]): void {
this._startVector = this._vector = points[0].sub(points[1]);
this._minDiameter = points[0].dist(points[1]);
}
_move(points: [Point, Point], pinchAround: Point | null, _e: TouchEvent): HandlerResult | void {
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): boolean {
/*
* The threshold before a rotation actually happens is configured in
* pixels along the 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: Point): boolean {
return Math.abs(vector.y) > Math.abs(vector.x);
}
const ALLOWED_SINGLE_TOUCH_TIME = 100;
/**
* The `TwoFingersTouchPitchHandler` allows the user to pitch the map by dragging up and down with two fingers.
*
* @group Handlers
*/
export class TwoFingersTouchPitchHandler extends TwoFingersTouchHandler {
_valid?: boolean;
_firstMove?: number;
_lastPoints?: [Point, Point];
_map: Map;
_currentTouchCount: number = 0;
constructor(map: Map) {
super();
this._map = map;
}
reset(): void {
super.reset();
this._valid = undefined;
delete this._firstMove;
delete this._lastPoints;
}
touchstart(e: TouchEvent, points: Array<Point>, mapTouches: Array<Touch>): void {
super.touchstart(e, points, mapTouches);
this._currentTouchCount = mapTouches.length;
}
_start(points: [Point, Point]): void {
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 | null, e: TouchEvent): HandlerResult | void {
// If cooperative gestures is enabled, we need a 3-finger minimum for this gesture to register
if (this._map.cooperativeGestures.isEnabled() && this._currentTouchCount < 3) {
return;
}
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): boolean | undefined {
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;
}
}