259 lines
8.3 KiB
JavaScript
259 lines
8.3 KiB
JavaScript
// Copyright 2014, Jason Davies, http://www.jasondavies.com
|
||
// See LICENSE.txt for details.
|
||
(function() {
|
||
|
||
var radians = Math.PI / 180,
|
||
degrees = 180 / Math.PI;
|
||
|
||
// TODO make incremental rotate optional
|
||
|
||
d3.geo.zoom = function() {
|
||
var projection,
|
||
duration;
|
||
|
||
var zoomPoint,
|
||
zooming = 0,
|
||
event = d3_eventDispatch(zoom, "zoomstart", "zoom", "zoomend"),
|
||
zoom = d3.behavior.zoom()
|
||
.on("zoomstart", function() {
|
||
var mouse0 = d3.mouse(this),
|
||
rotate = quaternionFromEuler(projection.rotate()),
|
||
point = position(projection, mouse0);
|
||
if (point) zoomPoint = point;
|
||
|
||
zoomOn.call(zoom, "zoom", function() {
|
||
projection.scale(view.k = d3.event.scale);
|
||
var mouse1 = d3.mouse(this),
|
||
between = rotateBetween(zoomPoint, position(projection, mouse1));
|
||
projection.rotate(view.r = eulerFromQuaternion(rotate = between
|
||
? multiply(rotate, between)
|
||
: multiply(bank(projection, mouse0, mouse1), rotate)));
|
||
mouse0 = mouse1;
|
||
zoomed(event.of(this, arguments));
|
||
});
|
||
zoomstarted(event.of(this, arguments));
|
||
})
|
||
.on("zoomend", function() {
|
||
zoomOn.call(zoom, "zoom", null);
|
||
zoomended(event.of(this, arguments));
|
||
}),
|
||
zoomOn = zoom.on,
|
||
view = {r: [0, 0, 0], k: 1};
|
||
|
||
zoom.rotateTo = function(location) {
|
||
var between = rotateBetween(cartesian(location), cartesian([-view.r[0], -view.r[1]]));
|
||
return eulerFromQuaternion(multiply(quaternionFromEuler(view.r), between));
|
||
};
|
||
|
||
zoom.projection = function(_) {
|
||
if (!arguments.length) return projection;
|
||
projection = _;
|
||
view = {r: projection.rotate(), k: projection.scale()};
|
||
return zoom.scale(view.k);
|
||
};
|
||
|
||
zoom.duration = function(_) {
|
||
return arguments.length ? (duration = _, zoom) : duration;
|
||
};
|
||
|
||
zoom.event = function(g) {
|
||
g.each(function() {
|
||
var g = d3.select(this),
|
||
dispatch = event.of(this, arguments),
|
||
view1 = view,
|
||
transition = d3.transition(g);
|
||
|
||
if (transition !== g) {
|
||
transition
|
||
.each("start.zoom", function() {
|
||
if (this.__chart__) { // pre-transition state
|
||
view = this.__chart__;
|
||
if (!view.hasOwnProperty("r")) view.r = projection.rotate();
|
||
}
|
||
projection.rotate(view.r).scale(view.k);
|
||
zoomstarted(dispatch);
|
||
})
|
||
.tween("zoom:zoom", function() {
|
||
var width = zoom.size()[0],
|
||
i = interpolateBetween(quaternionFromEuler(view.r), quaternionFromEuler(view1.r)),
|
||
d = d3.geo.distance(view.r, view1.r),
|
||
smooth = d3.interpolateZoom([0, 0, width / view.k], [d, 0, width / view1.k]);
|
||
if (duration) transition.duration(duration(smooth.duration * .001)); // see https://github.com/mbostock/d3/pull/2045
|
||
return function(t) {
|
||
var uw = smooth(t);
|
||
this.__chart__ = view = {r: eulerFromQuaternion(i(uw[0] / d)), k: width / uw[2]};
|
||
projection.rotate(view.r).scale(view.k);
|
||
zoom.scale(view.k);
|
||
zoomed(dispatch);
|
||
};
|
||
})
|
||
.each("end.zoom", function() {
|
||
zoomended(dispatch);
|
||
});
|
||
try { // see https://github.com/mbostock/d3/pull/1983
|
||
transition
|
||
.each("interrupt.zoom", function() {
|
||
zoomended(dispatch);
|
||
});
|
||
} catch (e) { console.log(e); }
|
||
} else {
|
||
this.__chart__ = view;
|
||
zoomstarted(dispatch);
|
||
zoomed(dispatch);
|
||
zoomended(dispatch);
|
||
}
|
||
});
|
||
};
|
||
|
||
function zoomstarted(dispatch) {
|
||
if (!zooming++) dispatch({type: "zoomstart"});
|
||
}
|
||
|
||
function zoomed(dispatch) {
|
||
dispatch({type: "zoom"});
|
||
}
|
||
|
||
function zoomended(dispatch) {
|
||
if (!--zooming) dispatch({type: "zoomend"});
|
||
}
|
||
|
||
return d3.rebind(zoom, event, "on");
|
||
};
|
||
|
||
function bank(projection, p0, p1) {
|
||
var t = projection.translate(),
|
||
angle = Math.atan2(p0[1] - t[1], p0[0] - t[0]) - Math.atan2(p1[1] - t[1], p1[0] - t[0]);
|
||
return [Math.cos(angle / 2), 0, 0, Math.sin(angle / 2)];
|
||
}
|
||
|
||
function position(projection, point) {
|
||
var spherical = projection.invert(point);
|
||
return spherical && isFinite(spherical[0]) && isFinite(spherical[1]) && cartesian(spherical);
|
||
}
|
||
|
||
function quaternionFromEuler(euler) {
|
||
var λ = .5 * euler[0] * radians,
|
||
φ = .5 * euler[1] * radians,
|
||
γ = .5 * euler[2] * radians,
|
||
sinλ = Math.sin(λ), cosλ = Math.cos(λ),
|
||
sinφ = Math.sin(φ), cosφ = Math.cos(φ),
|
||
sinγ = Math.sin(γ), cosγ = Math.cos(γ);
|
||
return [
|
||
cosλ * cosφ * cosγ + sinλ * sinφ * sinγ,
|
||
sinλ * cosφ * cosγ - cosλ * sinφ * sinγ,
|
||
cosλ * sinφ * cosγ + sinλ * cosφ * sinγ,
|
||
cosλ * cosφ * sinγ - sinλ * sinφ * cosγ
|
||
];
|
||
}
|
||
|
||
function multiply(a, b) {
|
||
var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3],
|
||
b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3];
|
||
return [
|
||
a0 * b0 - a1 * b1 - a2 * b2 - a3 * b3,
|
||
a0 * b1 + a1 * b0 + a2 * b3 - a3 * b2,
|
||
a0 * b2 - a1 * b3 + a2 * b0 + a3 * b1,
|
||
a0 * b3 + a1 * b2 - a2 * b1 + a3 * b0
|
||
];
|
||
}
|
||
|
||
function rotateBetween(a, b) {
|
||
if (!a || !b) return;
|
||
var axis = cross(a, b),
|
||
norm = Math.sqrt(dot(axis, axis)),
|
||
halfγ = .5 * Math.acos(Math.max(-1, Math.min(1, dot(a, b)))),
|
||
k = Math.sin(halfγ) / norm;
|
||
return norm && [Math.cos(halfγ), axis[2] * k, -axis[1] * k, axis[0] * k];
|
||
}
|
||
|
||
// Interpolate between two quaternions (slerp).
|
||
function interpolateBetween(a, b) {
|
||
var d = Math.max(-1, Math.min(1, dot(a, b))),
|
||
s = d < 0 ? -1 : 1,
|
||
θ = Math.acos(s * d),
|
||
sinθ = Math.sin(θ);
|
||
return sinθ ? function(t) {
|
||
var A = s * Math.sin((1 - t) * θ) / sinθ,
|
||
B = Math.sin(t * θ) / sinθ;
|
||
return [
|
||
a[0] * A + b[0] * B,
|
||
a[1] * A + b[1] * B,
|
||
a[2] * A + b[2] * B,
|
||
a[3] * A + b[3] * B
|
||
];
|
||
} : function() { return a; };
|
||
}
|
||
|
||
function eulerFromQuaternion(q) {
|
||
return [
|
||
Math.atan2(2 * (q[0] * q[1] + q[2] * q[3]), 1 - 2 * (q[1] * q[1] + q[2] * q[2])) * degrees,
|
||
Math.asin(Math.max(-1, Math.min(1, 2 * (q[0] * q[2] - q[3] * q[1])))) * degrees,
|
||
Math.atan2(2 * (q[0] * q[3] + q[1] * q[2]), 1 - 2 * (q[2] * q[2] + q[3] * q[3])) * degrees
|
||
];
|
||
}
|
||
|
||
function cartesian(spherical) {
|
||
var λ = spherical[0] * radians,
|
||
φ = spherical[1] * radians,
|
||
cosφ = Math.cos(φ);
|
||
return [
|
||
cosφ * Math.cos(λ),
|
||
cosφ * Math.sin(λ),
|
||
Math.sin(φ)
|
||
];
|
||
}
|
||
|
||
function dot(a, b) {
|
||
for (var i = 0, n = a.length, s = 0; i < n; ++i) s += a[i] * b[i];
|
||
return s;
|
||
}
|
||
|
||
function cross(a, b) {
|
||
return [
|
||
a[1] * b[2] - a[2] * b[1],
|
||
a[2] * b[0] - a[0] * b[2],
|
||
a[0] * b[1] - a[1] * b[0]
|
||
];
|
||
}
|
||
|
||
// Like d3.dispatch, but for custom events abstracting native UI events. These
|
||
// events have a target component (such as a brush), a target element (such as
|
||
// the svg:g element containing the brush) and the standard arguments `d` (the
|
||
// target element's data) and `i` (the selection index of the target element).
|
||
function d3_eventDispatch(target) {
|
||
var i = 0,
|
||
n = arguments.length,
|
||
argumentz = [];
|
||
|
||
while (++i < n) argumentz.push(arguments[i]);
|
||
|
||
var dispatch = d3.dispatch.apply(null, argumentz);
|
||
|
||
// Creates a dispatch context for the specified `thiz` (typically, the target
|
||
// DOM element that received the source event) and `argumentz` (typically, the
|
||
// data `d` and index `i` of the target element). The returned function can be
|
||
// used to dispatch an event to any registered listeners; the function takes a
|
||
// single argument as input, being the event to dispatch. The event must have
|
||
// a "type" attribute which corresponds to a type registered in the
|
||
// constructor. This context will automatically populate the "sourceEvent" and
|
||
// "target" attributes of the event, as well as setting the `d3.event` global
|
||
// for the duration of the notification.
|
||
dispatch.of = function(thiz, argumentz) {
|
||
return function(e1) {
|
||
try {
|
||
var e0 =
|
||
e1.sourceEvent = d3.event;
|
||
e1.target = target;
|
||
d3.event = e1;
|
||
dispatch[e1.type].apply(thiz, argumentz);
|
||
} finally {
|
||
d3.event = e0;
|
||
}
|
||
};
|
||
};
|
||
|
||
return dispatch;
|
||
}
|
||
|
||
})();
|