Files
iD/modules/util/zoom_pan.js
2020-04-02 12:40:41 -07:00

368 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Adapted from d3-zoom to handle pointer events.
// https://github.com/d3/d3-zoom/blob/523ccff340187a3e3c044eaa4d4a7391ea97272b/src/zoom.js
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { interpolateZoom } from 'd3-interpolate';
import { event as d3_event, customEvent as d3_customEvent } from 'd3-selection';
import { interrupt as d3_interrupt } from 'd3-transition';
import ZoomEvent from '../../node_modules/d3-zoom/src/event.js';
import { Transform, identity } from '../../node_modules/d3-zoom/src/transform.js';
import { utilFastMouse, utilFunctor } from './util';
// Ignore right-click, since that should open the context menu.
function defaultFilter() {
return !d3_event.ctrlKey && !d3_event.button;
}
function defaultExtent() {
var e = this;
if (e instanceof SVGElement) {
e = e.ownerSVGElement || e;
if (e.hasAttribute('viewBox')) {
e = e.viewBox.baseVal;
return [[e.x, e.y], [e.x + e.width, e.y + e.height]];
}
return [[0, 0], [e.width.baseVal.value, e.height.baseVal.value]];
}
return [[0, 0], [e.clientWidth, e.clientHeight]];
}
function defaultTransform() {
return this.__zoom || identity;
}
function defaultWheelDelta() {
return -d3_event.deltaY * (d3_event.deltaMode === 1 ? 0.05 : d3_event.deltaMode ? 1 : 0.002);
}
function defaultConstrain(transform, extent, translateExtent) {
var dx0 = transform.invertX(extent[0][0]) - translateExtent[0][0],
dx1 = transform.invertX(extent[1][0]) - translateExtent[1][0],
dy0 = transform.invertY(extent[0][1]) - translateExtent[0][1],
dy1 = transform.invertY(extent[1][1]) - translateExtent[1][1];
return transform.translate(
dx1 > dx0 ? (dx0 + dx1) / 2 : Math.min(0, dx0) || Math.max(0, dx1),
dy1 > dy0 ? (dy0 + dy1) / 2 : Math.min(0, dy0) || Math.max(0, dy1)
);
}
export function utilZoomPan() {
var filter = defaultFilter,
extent = defaultExtent,
constrain = defaultConstrain,
wheelDelta = defaultWheelDelta,
scaleExtent = [0, Infinity],
translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]],
interpolate = interpolateZoom,
listeners = d3_dispatch('start', 'zoom', 'end'),
_wheelDelay = 150;
function zoom(selection) {
selection
.property('__zoom', defaultTransform)
.on('pointerdown.zoom', pointerdown)
.on('pointermove.zoom', pointermove)
.on('pointerup.zoom pointercancel.zoom', pointerup)
.on('wheel.zoom', wheeled)
.style('touch-action', 'none')
.style('-webkit-tap-highlight-color', 'rgba(0,0,0,0)');
}
zoom.transform = function(collection, transform, point) {
var selection = collection.selection ? collection.selection() : collection;
selection.property('__zoom', defaultTransform);
if (collection !== selection) {
schedule(collection, transform, point);
} else {
selection.interrupt().each(function() {
gesture(this, arguments)
.start()
.zoom(null, typeof transform === 'function' ? transform.apply(this, arguments) : transform)
.end();
});
}
};
zoom.scaleBy = function(selection, k, p) {
zoom.scaleTo(selection, function() {
var k0 = this.__zoom.k,
k1 = typeof k === 'function' ? k.apply(this, arguments) : k;
return k0 * k1;
}, p);
};
zoom.scaleTo = function(selection, k, p) {
zoom.transform(selection, function() {
var e = extent.apply(this, arguments),
t0 = this.__zoom,
p0 = p == null ? centroid(e) : typeof p === 'function' ? p.apply(this, arguments) : p,
p1 = t0.invert(p0),
k1 = typeof k === 'function' ? k.apply(this, arguments) : k;
return constrain(translate(scale(t0, k1), p0, p1), e, translateExtent);
}, p);
};
zoom.translateBy = function(selection, x, y) {
zoom.transform(selection, function() {
return constrain(this.__zoom.translate(
typeof x === 'function' ? x.apply(this, arguments) : x,
typeof y === 'function' ? y.apply(this, arguments) : y
), extent.apply(this, arguments), translateExtent);
});
};
zoom.translateTo = function(selection, x, y, p) {
zoom.transform(selection, function() {
var e = extent.apply(this, arguments),
t = this.__zoom,
p0 = p == null ? centroid(e) : typeof p === 'function' ? p.apply(this, arguments) : p;
return constrain(identity.translate(p0[0], p0[1]).scale(t.k).translate(
typeof x === 'function' ? -x.apply(this, arguments) : -x,
typeof y === 'function' ? -y.apply(this, arguments) : -y
), e, translateExtent);
}, p);
};
function scale(transform, k) {
k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], k));
return k === transform.k ? transform : new Transform(k, transform.x, transform.y);
}
function translate(transform, p0, p1) {
var x = p0[0] - p1[0] * transform.k, y = p0[1] - p1[1] * transform.k;
return x === transform.x && y === transform.y ? transform : new Transform(transform.k, x, y);
}
function centroid(extent) {
return [(+extent[0][0] + +extent[1][0]) / 2, (+extent[0][1] + +extent[1][1]) / 2];
}
function schedule(transition, transform, point) {
transition
.on('start.zoom', function() { gesture(this, arguments).start(); })
.on('interrupt.zoom end.zoom', function() { gesture(this, arguments).end(); })
.tween('zoom', function() {
var that = this,
args = arguments,
g = gesture(that, args),
e = extent.apply(that, args),
p = point == null ? centroid(e) : typeof point === 'function' ? point.apply(that, args) : point,
w = Math.max(e[1][0] - e[0][0], e[1][1] - e[0][1]),
a = that.__zoom,
b = typeof transform === 'function' ? transform.apply(that, args) : transform,
i = interpolate(a.invert(p).concat(w / a.k), b.invert(p).concat(w / b.k));
return function(t) {
if (t === 1) t = b; // Avoid rounding error on end.
else { var l = i(t), k = w / l[2]; t = new Transform(k, p[0] - l[0] * k, p[1] - l[1] * k); }
g.zoom(null, t);
};
});
}
function gesture(that, args, clean) {
return (!clean && that.__zooming) || new Gesture(that, args);
}
function Gesture(that, args) {
this.that = that;
this.args = args;
this.active = 0;
this.extent = extent.apply(that, args);
}
Gesture.prototype = {
start: function() {
if (++this.active === 1) {
this.that.__zooming = this;
this.emit('start');
}
return this;
},
zoom: function(key, transform) {
if (this.mouse && key !== 'mouse') this.mouse[1] = transform.invert(this.mouse[0]);
if (this.pointer0 && key !== 'touch') this.pointer0[1] = transform.invert(this.pointer0[0]);
if (this.pointer1 && key !== 'touch') this.pointer1[1] = transform.invert(this.pointer1[0]);
this.that.__zoom = transform;
this.emit('zoom');
return this;
},
end: function() {
if (--this.active === 0) {
delete this.that.__zooming;
this.emit('end');
}
return this;
},
emit: function(type) {
d3_customEvent(new ZoomEvent(zoom, type, this.that.__zoom), listeners.apply, listeners, [type, this.that, this.args]);
}
};
function wheeled() {
if (!filter.apply(this, arguments)) return;
var g = gesture(this, arguments),
t = this.__zoom,
k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], t.k * Math.pow(2, wheelDelta.apply(this, arguments)))),
p = utilFastMouse(this)(d3_event);
// If the mouse is in the same location as before, reuse it.
// If there were recent wheel events, reset the wheel idle timeout.
if (g.wheel) {
if (g.mouse[0][0] !== p[0] || g.mouse[0][1] !== p[1]) {
g.mouse[1] = t.invert(g.mouse[0] = p);
}
clearTimeout(g.wheel);
}
// If this wheel event wont trigger a transform change, ignore it.
else if (t.k === k) return;
// Otherwise, capture the mouse point and location at the start.
else {
g.mouse = [p, t.invert(p)];
d3_interrupt(this);
g.start();
}
d3_event.preventDefault();
d3_event.stopImmediatePropagation();
g.wheel = setTimeout(wheelidled, _wheelDelay);
g.zoom('mouse', constrain(translate(scale(t, k), g.mouse[0], g.mouse[1]), g.extent, translateExtent));
function wheelidled() {
g.wheel = null;
g.end();
}
}
var _downPointerIDs = new Set();
var _pointerLocGetter;
function pointerdown() {
_downPointerIDs.add(d3_event.pointerId);
if (!filter.apply(this, arguments)) return;
var g = gesture(this, arguments, _downPointerIDs.size === 1);
var started;
d3_event.stopImmediatePropagation();
_pointerLocGetter = utilFastMouse(this);
var loc = _pointerLocGetter(d3_event);
var p = [loc, this.__zoom.invert(loc), d3_event.pointerId];
if (!g.pointer0) {
g.pointer0 = p;
started = true;
} else if (!g.pointer1 && g.pointer0[2] !== p[2]) {
g.pointer1 = p;
}
if (started) {
d3_interrupt(this);
g.start();
}
}
function pointermove() {
if (!this.__zooming || !_pointerLocGetter) return;
var g = gesture(this, arguments);
var isPointer0 = g.pointer0 && g.pointer0[2] === d3_event.pointerId;
var isPointer1 = !isPointer0 && g.pointer1 && g.pointer1[2] === d3_event.pointerId;
if ((isPointer0 || isPointer1) && 'buttons' in d3_event && !d3_event.buttons) {
// The pointer went up without ending the gesture somehow, e.g.
// a down mouse was moved off the map and released. End it here.
if (g.pointer0) _downPointerIDs.delete(g.pointer0[2]);
if (g.pointer1) _downPointerIDs.delete(g.pointer1[2]);
g.end();
return;
}
d3_event.preventDefault();
d3_event.stopImmediatePropagation();
var loc = _pointerLocGetter(d3_event);
var t, p, l;
if (isPointer0) g.pointer0[0] = loc;
else if (isPointer1) g.pointer1[0] = loc;
t = g.that.__zoom;
if (g.pointer1) {
var p0 = g.pointer0[0], l0 = g.pointer0[1],
p1 = g.pointer1[0], l1 = g.pointer1[1],
dp = (dp = p1[0] - p0[0]) * dp + (dp = p1[1] - p0[1]) * dp,
dl = (dl = l1[0] - l0[0]) * dl + (dl = l1[1] - l0[1]) * dl;
t = scale(t, Math.sqrt(dp / dl));
p = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2];
l = [(l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2];
} else if (g.pointer0) {
p = g.pointer0[0];
l = g.pointer0[1];
}
else return;
g.zoom('touch', constrain(translate(t, p, l), g.extent, translateExtent));
}
function pointerup() {
_downPointerIDs.delete(d3_event.pointerId);
if (!this.__zooming) return;
var g = gesture(this, arguments);
d3_event.stopImmediatePropagation();
if (g.pointer0 && g.pointer0[2] === d3_event.pointerId) delete g.pointer0;
else if (g.pointer1 && g.pointer1[2] === d3_event.pointerId) delete g.pointer1;
if (g.pointer1 && !g.pointer0) {
g.pointer0 = g.pointer1;
delete g.pointer1;
}
if (g.pointer0) g.pointer0[1] = this.__zoom.invert(g.pointer0[0]);
else {
g.end();
}
}
zoom.wheelDelta = function(_) {
return arguments.length ? (wheelDelta = utilFunctor(+_), zoom) : wheelDelta;
};
zoom.filter = function(_) {
return arguments.length ? (filter = utilFunctor(!!_), zoom) : filter;
};
zoom.extent = function(_) {
return arguments.length ? (extent = utilFunctor([[+_[0][0], +_[0][1]], [+_[1][0], +_[1][1]]]), zoom) : extent;
};
zoom.scaleExtent = function(_) {
return arguments.length ? (scaleExtent[0] = +_[0], scaleExtent[1] = +_[1], zoom) : [scaleExtent[0], scaleExtent[1]];
};
zoom.translateExtent = function(_) {
return arguments.length ? (translateExtent[0][0] = +_[0][0], translateExtent[1][0] = +_[1][0], translateExtent[0][1] = +_[0][1], translateExtent[1][1] = +_[1][1], zoom) : [[translateExtent[0][0], translateExtent[0][1]], [translateExtent[1][0], translateExtent[1][1]]];
};
zoom.constrain = function(_) {
return arguments.length ? (constrain = _, zoom) : constrain;
};
zoom.interpolate = function(_) {
return arguments.length ? (interpolate = _, zoom) : interpolate;
};
zoom.on = function() {
var value = listeners.on.apply(listeners, arguments);
return value === listeners ? zoom : value;
};
return zoom;
}