mapbox-gl
Version:
A WebGL interactive maps library
361 lines (302 loc) • 10.5 kB
JavaScript
;
var Evented = require('../util/evented');
var browser = require('../util/browser');
var Point = require('point-geometry');
module.exports = Interaction;
/**
* Mouse event
*
* @event mousemove
* @memberof Map
* @type {Object}
* @property {Point} point the pixel location of the event
* @property {Event} originalEvent the original DOM event
*/
/**
* Double click event.
*
* @event dblclick
* @memberof Map
* @type {Object}
* @property {Point} point the pixel location of the event
*/
/**
* Pan event
*
* @event pan
* @memberof Map
* @type {Object}
* @property {Point} point the pixel location of the event
* @property {Point} offset a point representing the movement from the previous map location to the current one.
*/
/**
* Pan end event
*
* @event panend
* @memberof Map
* @type {Object}
* @property {number} velocity a measure of how much inertia was recorded in this pan motion
*/
function Interaction(el) {
var interaction = this;
if (!el) return;
var rotating = false,
panned = false,
boxzoom = false,
firstPos = null,
pos = null,
inertia = null,
now;
function mousePos(e) {
var rect = el.getBoundingClientRect();
e = e.touches ? e.touches[0] : e;
return new Point(
e.clientX - rect.left - el.clientLeft,
e.clientY - rect.top - el.clientTop);
}
el.addEventListener('contextmenu', function(ev) {
rotating = true;
firstPos = pos = mousePos(ev);
ev.preventDefault();
}, false);
el.addEventListener('mousedown', onmousedown, false);
el.addEventListener('touchstart', ontouchstart, false);
document.addEventListener('mouseup', onmouseup, false);
document.addEventListener('touchend', onmouseup, false);
document.addEventListener('mousemove', onmousemove, false);
document.addEventListener('touchmove', ontouchmove, false);
el.addEventListener('click', onclick, false);
scrollwheel(zoom);
el.addEventListener('dblclick', ondoubleclick, false);
window.addEventListener('resize', resize, false);
el.addEventListener('keydown', keydown, false);
function zoom(type, delta, point) {
interaction.fire('zoom', {
source: type,
delta: delta,
point: point
});
inertia = null;
now = null;
}
function click(point, ev) {
interaction.fire('click', {point: point, originalEvent: ev});
}
function pinch(scale, bearing, point) {
interaction.fire('pinch', {
scale: scale,
bearing: bearing,
point: point
});
inertia = null;
now = null;
}
function mousemove(point, ev) {
interaction.fire('mousemove', {point: point, originalEvent: ev});
}
function pan(point) {
if (pos) {
var offset = pos.sub(point);
interaction.fire('pan', {offset: offset, point: point});
// add an averaged version of this movement to the inertia vector
if (inertia) {
var duration = Date.now() - now;
// sometimes it's 0 after some erratic paning
if (duration) {
var time = duration + now;
inertia.push([time, point]);
while (inertia.length > 2 && time - inertia[0][0] > 100) inertia.shift();
}
} else {
inertia = [];
}
now = Date.now();
pos = point;
}
}
function resize() {
interaction.fire('resize');
}
function keydown(ev) {
if (boxzoom && ev.keyCode === 27) {
interaction.fire('boxzoomcancel');
boxzoom = false;
}
interaction.fire('keydown', ev);
}
function rotate(point) {
if (pos) {
interaction.fire('rotate', {
start: firstPos,
prev: pos,
current: point
});
pos = point;
}
}
function doubleclick(point, ev) {
interaction.fire('dblclick', {
point: point,
originalEvent: ev
});
}
function onmousedown(ev) {
firstPos = pos = mousePos(ev);
interaction.fire('down');
if (ev.shiftKey || ((ev.which === 1) && (ev.button === 1))) {
boxzoom = true;
}
}
function onmouseup(ev) {
panned = pos && firstPos && (pos.x !== firstPos.x || pos.y !== firstPos.y);
rotating = false;
pos = null;
if (boxzoom) {
interaction.fire('boxzoomend', {
start: firstPos,
current: mousePos(ev)
});
boxzoom = false;
} else if (inertia && inertia.length >= 2 && now > Date.now() - 100) {
var last = inertia[inertia.length - 1],
first = inertia[0],
velocity = last[1].sub(first[1]).div(last[0] - first[0]);
interaction.fire('panend', {inertia: velocity});
} else if (pos) {
interaction.fire('panend');
}
inertia = null;
now = null;
}
function onmousemove(ev) {
var point = mousePos(ev);
if (boxzoom) {
interaction.fire('boxzoomstart', {
start: firstPos,
current: point
});
} else if (rotating) {
rotate(point);
} else if (pos) {
pan(point);
} else {
var target = ev.toElement || ev.target;
while (target && target !== el && target.parentNode) target = target.parentNode;
if (target === el) {
mousemove(point, ev);
}
}
}
function onclick(ev) {
if (!panned) click(mousePos(ev), ev);
}
function ondoubleclick(ev) {
doubleclick(mousePos(ev), ev);
zoom('wheel', Infinity * (ev.shiftKey ? -1 : 1), mousePos(ev));
ev.preventDefault();
}
var startVec;
var tapped;
function ontouchstart(e) {
if (e.touches.length === 1) {
onmousedown(e);
if (!tapped) {
tapped = setTimeout(function() {
tapped = null;
}, 300);
} else {
clearTimeout(tapped);
tapped = null;
ondoubleclick(e);
}
} else if (e.touches.length === 2) {
startVec = mousePos(e.touches[0]).sub(mousePos(e.touches[1]));
interaction.fire('pinchstart');
}
}
function ontouchmove(e) {
if (e.touches.length === 1) {
onmousemove(e);
} else if (e.touches.length === 2) {
var p1 = mousePos(e.touches[0]),
p2 = mousePos(e.touches[1]),
p = p1.add(p2).div(2),
vec = p1.sub(p2),
scale = vec.mag() / startVec.mag(),
bearing = vec.angleWith(startVec) * 180 / Math.PI;
pinch(scale, bearing, p);
}
e.preventDefault();
}
function scrollwheel(callback) {
var firefox = /Firefox/i.test(navigator.userAgent);
var safari = /Safari/i.test(navigator.userAgent) && !/Chrom(ium|e)/i.test(navigator.userAgent);
var time = window.performance || Date;
el.addEventListener('wheel', wheel, false);
el.addEventListener('mousewheel', mousewheel, false);
var lastEvent = 0;
var type = null;
var typeTimeout = null;
var initialValue = null;
function scroll(value, ev) {
var stamp = time.now();
var timeDelta = stamp - lastEvent;
lastEvent = stamp;
var point = mousePos(ev);
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;
initialValue = value;
// Start a timeout in case this was a singular event, and dely it
// by up to 40ms.
typeTimeout = setTimeout(function() {
type = 'wheel';
callback(type, -initialValue, point);
}, 40);
} else if (type === null) {
// 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 (typeTimeout) {
clearTimeout(typeTimeout);
typeTimeout = null;
value += initialValue;
}
}
// Slow down zoom if shift key is held for more precise zooming
if (ev.shiftKey && value) value = value / 4;
// Only fire the callback if we actually know what type of scrolling
// device the user uses.
if (type !== null) {
callback(type, -value, point);
}
}
function wheel(e) {
var deltaY = e.deltaY;
// Firefox doubles the values on retina screens...
if (firefox && e.deltaMode === window.WheelEvent.DOM_DELTA_PIXEL) deltaY /= browser.devicePixelRatio;
if (e.deltaMode === window.WheelEvent.DOM_DELTA_LINE) deltaY *= 40;
scroll(deltaY, e);
e.preventDefault();
}
function mousewheel(e) {
var deltaY = -e.wheelDeltaY;
if (safari) deltaY = deltaY / 3;
scroll(deltaY, e);
e.preventDefault();
}
}
}
Interaction.prototype = Object.create(Evented);