// 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), rotate0 = projection.rotate(), translate0 = projection.translate(), q = 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), point1 = position(projection, mouse1), between = rotateBetween(zoomPoint, point1); var rotateAngles = view.r = eulerFromQuaternion(q = between ? multiply(q, between) : multiply(bank(projection, mouse0, mouse1), q)); projection.rotate([ rotateAngles[0], rotateAngles[1], rotate0[2] ]); 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()}; 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 * .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 λ = .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 // 0 ]; } 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; } })();