All Downloads are FREE. Search and download functionalities are using the official Maven repository.

package.src.ui.handler.touch_zoom_rotate.js Maven / Gradle / Ivy

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();
     */
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy