Skip to content

Instantly share code, notes, and snippets.

@alexcjohnson
Last active August 29, 2015 14:19
Show Gist options
  • Save alexcjohnson/bfef279fca09a6e3f8ba to your computer and use it in GitHub Desktop.
Save alexcjohnson/bfef279fca09a6e3f8ba to your computer and use it in GitHub Desktop.
map panning with no roll
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.zoomlayer {
fill: transparent;
}
.land {
fill: rgb(83, 207, 142);
}
.graticule {
fill: none;
stroke: grey;
}
</style>
<body>
<svg class='fig'></svg>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/d3.geo.projection.v0.min.js" charset="utf-8"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script src="zoom_3b.js"></script>
<script>
(function () {
var pathToTopojson = './world110.json' // or world50.json
d3.json(pathToTopojson, main);
function main(err, world) {
var width = 720,
height = 400;
var countries = topojson.feature(world, world.objects.land);
// var projection = d3.geo.kavrayskiy7()
// .scale(120)
// .rotate([0, 0, 0]);
var projection = d3.geo.orthographic()
.clipAngle(90)
.scale(170)
.rotate([0, 0, 0]);
projection.translate([width / 2, height / 2]);
window.projection = projection;
var path = d3.geo.path()
.projection(projection);
var graticule = d3.geo.graticule();
var svg = d3.select('.fig')
.attr('height', height)
.attr('width', width);
var g = svg.append("g")
.attr("class", "map");
g.append("rect")
.attr("class", "zoomlayer")
.attr("width", width)
.attr("height", height);
g.append("path")
.datum(countries)
.attr("d", path)
.attr("class", "land");
g.append("path")
.datum(graticule)
.attr("d", path)
.attr("class", "graticule");
var zoom = d3.geo.zoom()
.projection(projection)
.on("zoom.redraw", function() {
svg.selectAll("path")
.attr("d", path);
});
g
.call(zoom)
.on("dblclick.zoom", null);
}
})();
</script>
</body>
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
// Copyright 2014, Jason Davies, http://www.jasondavies.com
// See LICENSE.txt for details.
(function() {
/* global d3:false */
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),
rotate0 = projection.rotate(),
lastRotate = rotate0,
translate0 = projection.translate(),
q = quaternionFromEuler(rotate0);
zoomPoint = position(projection, mouse0);
zoomOn.call(zoom, 'zoom', function() {
var mouse1 = d3.mouse(this);
projection.scale(view.k = d3.event.scale);
if(!zoomPoint) {
// if no zoomPoint, the mouse wasn't over the actual geography yet
// maybe this point is the start... we'll find out next time!
mouse0 = mouse1;
zoomPoint = position(projection, mouse0);
}
// check if the point is on the map
// if not, don't do anything new but scale
// if it is, then we can assume between will exist below
// so we don't need the 'bank' function, whatever that is.
// TODO: is this right?
else if(position(projection, mouse1)) {
// go back to original projection temporarily
// except for scale... that's kind of independent?
projection
.rotate(rotate0)
.translate(translate0);
// calculate the new params
var point1 = position(projection, mouse1),
between = rotateBetween(zoomPoint, point1),
newEuler = eulerFromQuaternion(multiply(q, between)),
rotateAngles = view.r = unRoll(newEuler, zoomPoint, lastRotate);
if(!isFinite(rotateAngles[0]) || !isFinite(rotateAngles[1]) ||
!isFinite(rotateAngles[2])) {
rotateAngles = lastRotate;
}
// update the projection
projection.rotate(rotateAngles);
lastRotate = rotateAngles;
}
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()};
zoom.translate(projection.translate());
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__;
// }
// 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 * 0.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 (ignore) { }
// } 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 lambda = 0.5 * euler[0] * radians,
phi = 0.5 * euler[1] * radians,
gamma = 0.5 * euler[2] * radians,
sinLambda = Math.sin(lambda), cosLambda = Math.cos(lambda),
sinPhi = Math.sin(phi), cosPhi = Math.cos(phi),
sinGamma = Math.sin(gamma), cosGamma = Math.cos(gamma);
return [
cosLambda * cosPhi * cosGamma + sinLambda * sinPhi * sinGamma,
sinLambda * cosPhi * cosGamma - cosLambda * sinPhi * sinGamma,
cosLambda * sinPhi * cosGamma + sinLambda * cosPhi * sinGamma,
cosLambda * cosPhi * sinGamma - sinLambda * sinPhi * cosGamma
];
}
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)),
halfgamma = 0.5 * Math.acos(Math.max(-1, Math.min(1, dot(a, b)))),
k = Math.sin(halfgamma) / norm;
return norm && [Math.cos(halfgamma), axis[2] * k, -axis[1] * k, axis[0] * k];
}
// input:
// rotateAngles: a calculated set of Euler angles
// pt: a point (cartesian in 3-space) to keep fixed
// roll0: an initial roll, to be preserved
// output:
// a set of Euler angles that preserve the projection of pt
// but set roll (output[2]) equal to roll0
// note that this doesn't depend on the particular projection,
// just on the rotation angles
function unRoll(rotateAngles, pt, lastRotate) {
// calculate the fixed point transformed by these Euler angles
// but with the desired roll undone
var ptRotated = rotateCartesian(pt, 2, rotateAngles[0]);
ptRotated = rotateCartesian(ptRotated, 1, rotateAngles[1]);
ptRotated = rotateCartesian(ptRotated, 0, rotateAngles[2] - lastRotate[2]);
var x = pt[0],
y = pt[1],
z = pt[2],
f = ptRotated[0],
g = ptRotated[1],
h = ptRotated[2],
// the following essentially solves:
// ptRotated = rotateCartesian(rotateCartesian(pt, 2, newYaw), 1, newPitch)
// for newYaw and newPitch, as best it can
theta = Math.atan2(y, x) * degrees,
a = Math.sqrt(x * x + y * y),
b,
newYaw1;
if(Math.abs(g) > a) {
newYaw1 = (g > 0 ? 90 : -90) - theta;
b = 0;
} else {
newYaw1 = Math.asin(g / a) * degrees - theta;
b = Math.sqrt(a * a - g * g);
}
var newYaw2 = 180 - newYaw1 - 2*theta,
newPitch1 = (Math.atan2(h, f) - Math.atan2(z, b)) * degrees,
newPitch2 = (Math.atan2(h, f) - Math.atan2(z, -b)) * degrees;
// which is closest to lastRotate[0,1]: newYaw/Pitch or newYaw2/Pitch2?
var dist1 = angleDistance(lastRotate[0], lastRotate[1], newYaw1, newPitch1),
dist2 = angleDistance(lastRotate[0], lastRotate[1], newYaw2, newPitch2);
if(dist1 <= dist2) return [newYaw1, newPitch1, lastRotate[2]];
else return [newYaw2, newPitch2, lastRotate[2]];
}
function angleDistance(yaw0, pitch0, yaw1, pitch1) {
var dYaw = angleMod(yaw1 - yaw0),
dPitch = angleMod(pitch1 - pitch0);
return Math.sqrt(dYaw * dYaw + dPitch * dPitch);
}
// reduce an angle in degrees to [-180,180]
function angleMod(angle) {
return (angle % 360 + 540) %360 - 180;
}
// rotate a cartesian vector
// axis is 0 (x), 1 (y), or 2 (z)
// angle is in degrees
function rotateCartesian(vector, axis, angle) {
var angleRads = angle * radians,
vectorOut = vector.slice(),
ax1 = (axis===0) ? 1 : 0,
ax2 = (axis===2) ? 1 : 2,
cosa = Math.cos(angleRads),
sina = Math.sin(angleRads);
vectorOut[ax1] = vector[ax1] * cosa - vector[ax2] * sina;
vectorOut[ax2] = vector[ax2] * cosa + vector[ax1] * sina;
return vectorOut;
}
// 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,
// theta = Math.acos(s * d),
// sinTheta = Math.sin(theta);
// return sinTheta ? function(t) {
// var A = s * Math.sin((1 - t) * theta) / sinTheta,
// B = Math.sin(t * theta) / sinTheta;
// 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 lambda = spherical[0] * radians,
phi = spherical[1] * radians,
cosPhi = Math.cos(phi);
return [
cosPhi * Math.cos(lambda),
cosPhi * Math.sin(lambda),
Math.sin(phi)
];
}
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) {
var e0;
try {
e0 = e1.sourceEvent = d3.event;
e1.target = target;
d3.event = e1;
dispatch[e1.type].apply(thiz, argumentz);
} finally {
d3.event = e0;
}
};
};
return dispatch;
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment