|
// this isn't structured like a proper behavior. something |
|
// that would probably be useful, for now I just want to encapsulate it |
|
// all the code below comes from http://bl.ocks.org/patricksurry/5721459 |
|
|
|
d3.behavior.trackball = function(svg) { |
|
|
|
svg |
|
.on("mousedown", mousedown) |
|
.on("mousemove", mousemove) |
|
.on("mouseup", mouseup); |
|
|
|
function trackballAngles(pt) { |
|
// based on http://www.opengl.org/wiki/Trackball |
|
// given a click at (x,y) in canvas coords on the globe (trackball), |
|
// calculate the spherical coordianates for the point as a rotation around |
|
// the vertical and horizontal axes |
|
|
|
var r = projection.scale(); |
|
var c = projection.translate(); |
|
var x = pt[0] - c[0], y = - (pt[1] - c[1]), ss = x*x + y*y; |
|
|
|
|
|
var z = r*r > 2 * ss ? Math.sqrt(r*r - ss) : r*r / 2 / Math.sqrt(ss); |
|
|
|
var lambda = Math.atan2(x, z) * 180 / Math.PI; |
|
var phi = Math.atan2(y, z) * 180 / Math.PI |
|
return [lambda, phi]; |
|
} |
|
|
|
/* |
|
This is the cartesian equivalent of the rotation matrix, |
|
which is the product of the following rotations (in numbered order): |
|
1. longitude: λ around the y axis (which points up in the canvas) |
|
2. latitude: -ϕ around the x axis (which points right in the canvas) |
|
3. yaw: γ around the z axis (which points out of the screen) |
|
|
|
NB. If you measure rotations in a positive direction according to the right-hand rule |
|
(point your right thumb in the positive direction of the rotation axis, and rotate in the |
|
direction of your curled fingers), then the latitude rotation is negative. |
|
|
|
R(λ, ϕ, γ) = |
|
[[ sin(γ)sin(λ)sin(ϕ)+cos(γ)cos(λ), −sin(γ)cos(ϕ), −sin(γ)sin(ϕ)cos(λ)+sin(λ)cos(γ)], |
|
[ −sin(λ)sin(ϕ)cos(γ)+sin(γ)cos(λ), cos(γ)cos(ϕ), sin(ϕ)cos(γ)cos(λ)+sin(γ)sin(λ)], |
|
[ −sin(λ)cos(ϕ), −sin(ϕ), cos(λ)cos(ϕ)]] |
|
|
|
If you then apply a "trackball rotation" of δλ around the y axis, and -δϕ around the |
|
x axis, you get this horrible composite matrix: |
|
|
|
R2(λ, ϕ, γ, δλ, δϕ) = |
|
[[−sin(δλ)sin(λ)cos(ϕ)+(sin(γ)sin(λ)sin(ϕ)+cos(γ)cos(λ))cos(δλ), |
|
−sin(γ)cos(δλ)cos(ϕ)−sin(δλ)sin(ϕ), |
|
sin(δλ)cos(λ)cos(ϕ)−(sin(γ)sin(ϕ)cos(λ)−sin(λ)cos(γ))cos(δλ)], |
|
[−sin(δϕ)sin(λ)cos(δλ)cos(ϕ)−(sin(γ)sin(λ)sin(ϕ)+cos(γ)cos(λ))sin(δλ)sin(δϕ)−(sin(λ)sin(ϕ)cos(γ)−sin(γ)cos(λ))cos(δϕ), |
|
sin(δλ)sin(δϕ)sin(γ)cos(ϕ)−sin(δϕ)sin(ϕ)cos(δλ)+cos(δϕ)cos(γ)cos(ϕ), |
|
sin(δϕ)cos(δλ)cos(λ)cos(ϕ)+(sin(γ)sin(ϕ)cos(λ)−sin(λ)cos(γ))sin(δλ)sin(δϕ)+(sin(ϕ)cos(γ)cos(λ)+sin(γ)sin(λ))cos(δϕ)], |
|
[−sin(λ)cos(δλ)cos(δϕ)cos(ϕ)−(sin(γ)sin(λ)sin(ϕ)+cos(γ)cos(λ))sin(δλ)cos(δϕ)+(sin(λ)sin(ϕ)cos(γ)−sin(γ)cos(λ))sin(δϕ), |
|
sin(δλ)sin(γ)cos(δϕ)cos(ϕ)−sin(δϕ)cos(γ)cos(ϕ)−sin(ϕ)cos(δλ)cos(δϕ), |
|
cos(δλ)cos(δϕ)cos(λ)cos(ϕ)+(sin(γ)sin(ϕ)cos(λ)−sin(λ)cos(γ))sin(δλ)cos(δϕ)−(sin(ϕ)cos(γ)cos(λ)+sin(γ)sin(λ))sin(δϕ)]] |
|
|
|
by equating components of the matrics |
|
(label them [[a00, a01, a02], [a10, a11, a12], [a20, a21, a22]]) |
|
we can find an equivalent rotation R(λ', ϕ', γ') == RC(λ, ϕ, γ, δλ, δϕ) : |
|
|
|
if cos(ϕ') != 0: |
|
γ' = atan2(-RC01, RC11) |
|
ϕ' = atan2(-RC21, γ' == 0 ? RC11 / cos(γ') : - RC01 / sin(γ')) |
|
λ' = atan2(-RC20, RC22) |
|
else: |
|
// when cos(ϕ') == 0, RC21 == - sin(ϕ') == +/- 1 |
|
// the solution is degenerate, requiring just that |
|
// γ' - λ' = atan2(RC00, RC10) if RC21 == -1 (ϕ' = π/2) |
|
// or γ' + λ' = atan2(RC00, RC10) if RC21 == 1 (ϕ' = -π/2) |
|
// so choose: |
|
γ' = atan2(RC10, RC00) - RC21 * λ |
|
ϕ' = - RC21 * π/2 |
|
λ' = λ |
|
|
|
*/ |
|
|
|
function composedRotation(λ, ϕ, γ, δλ, δϕ) { |
|
λ = Math.PI / 180 * λ; |
|
ϕ = Math.PI / 180 * ϕ; |
|
γ = Math.PI / 180 * γ; |
|
δλ = Math.PI / 180 * δλ; |
|
δϕ = Math.PI / 180 * δϕ; |
|
|
|
var sλ = Math.sin(λ), sϕ = Math.sin(ϕ), sγ = Math.sin(γ), |
|
sδλ = Math.sin(δλ), sδϕ = Math.sin(δϕ), |
|
cλ = Math.cos(λ), cϕ = Math.cos(ϕ), cγ = Math.cos(γ), |
|
cδλ = Math.cos(δλ), cδϕ = Math.cos(δϕ); |
|
|
|
var m00 = -sδλ * sλ * cϕ + (sγ * sλ * sϕ + cγ * cλ) * cδλ, |
|
m01 = -sγ * cδλ * cϕ - sδλ * sϕ, |
|
m02 = sδλ * cλ * cϕ - (sγ * sϕ * cλ - sλ * cγ) * cδλ, |
|
m10 = - sδϕ * sλ * cδλ * cϕ - (sγ * sλ * sϕ + cγ * cλ) * sδλ * sδϕ - (sλ * sϕ * cγ - sγ * cλ) * cδϕ, |
|
m11 = sδλ * sδϕ * sγ * cϕ - sδϕ * sϕ * cδλ + cδϕ * cγ * cϕ, |
|
m12 = sδϕ * cδλ * cλ * cϕ + (sγ * sϕ * cλ - sλ * cγ) * sδλ * sδϕ + (sϕ * cγ * cλ + sγ * sλ) * cδϕ, |
|
m20 = - sλ * cδλ * cδϕ * cϕ - (sγ * sλ * sϕ + cγ * cλ) * sδλ * cδϕ + (sλ * sϕ * cγ - sγ * cλ) * sδϕ, |
|
m21 = sδλ * sγ * cδϕ * cϕ - sδϕ * cγ * cϕ - sϕ * cδλ * cδϕ, |
|
m22 = cδλ * cδϕ * cλ * cϕ + (sγ * sϕ * cλ - sλ * cγ) * sδλ * cδϕ - (sϕ * cγ * cλ + sγ * sλ) * sδϕ; |
|
|
|
if (m01 != 0 || m11 != 0) { |
|
γ_ = Math.atan2(-m01, m11); |
|
ϕ_ = Math.atan2(-m21, Math.sin(γ_) == 0 ? m11 / Math.cos(γ_) : - m01 / Math.sin(γ_)); |
|
λ_ = Math.atan2(-m20, m22); |
|
} else { |
|
γ_ = Math.atan2(m10, m00) - m21 * λ; |
|
ϕ_ = - m21 * Math.PI / 2; |
|
λ_ = λ; |
|
} |
|
|
|
return([λ_ * 180 / Math.PI, ϕ_ * 180 / Math.PI, γ_ * 180 / Math.PI]); |
|
} |
|
|
|
var m0 = null, |
|
o0; |
|
|
|
var dispatch = d3.dispatch("rotate") |
|
function mousedown() { // remember where the mouse was pressed, in canvas coords |
|
m0 = trackballAngles(d3.mouse(svg[0][0])); |
|
o0 = projection.rotate(); |
|
d3.event.preventDefault(); |
|
} |
|
|
|
function mousemove() { |
|
if (m0) { // if mousedown |
|
var m1 = trackballAngles(d3.mouse(svg[0][0])); |
|
// we want to find rotate the current projection so that the point at m0 rotates to m1 |
|
// along the great circle arc between them. |
|
// when the current projection is at rotation(0,0), with the north pole aligned |
|
// to the vertical canvas axis, and the equator aligned to the horizontal canvas |
|
// axis, this is easy to do, since D3's longitude rotation corresponds to trackball |
|
// rotation around the vertical axis, and then the subsequent latitude rotation |
|
// corresponds to the trackball rotation around the horizontal axis. |
|
// But if the current projection is already rotated, it's harder. |
|
// We need to find a new rotation equivalent to the composition of both |
|
|
|
// Choose one of these three update schemes: |
|
|
|
// Best behavior |
|
o1 = composedRotation(o0[0], o0[1], o0[2], m1[0] - m0[0], m1[1] - m0[1]) |
|
|
|
// Improved behavior over original example |
|
//o1 = [o0[0] + (m1[0] - m0[0]), o0[1] + (m1[1] - m0[1])]; |
|
|
|
// Original example from http://mbostock.github.io/d3/talk/20111018/azimuthal.html |
|
// o1 = [o0[0] - (m0[0] - m1[0]) / 8, o0[1] - (m1[1] - m0[1]) / 8]; |
|
|
|
// move to the updated rotation |
|
dispatch.rotate(o1); |
|
//projection.rotate(o1); |
|
|
|
// We can optionally update the "origin state" at each step. This has the |
|
// advantage that each 'trackball movement' is small, but the disadvantage of |
|
// potentially accumulating many small drifts (you often see a twist creeping in |
|
// if you keep rolling the globe around with the mouse button down) |
|
// o0 = o1; |
|
// m0 = m1; |
|
|
|
//svg.selectAll("path").attr("d", path); |
|
} |
|
} |
|
|
|
function mouseup() { |
|
if (m0) { |
|
mousemove(); |
|
m0 = null; |
|
} |
|
} |
|
|
|
return dispatch; |
|
} |