spacedb/static/js/lib/d3.geo.zoom.js
2025-12-08 15:12:52 -05:00

259 lines
8.3 KiB
JavaScript
Raw Permalink 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.

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