// Copyright (c) 2013, 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, zoomPoint, event = d3.dispatch("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(d3.event.scale); var mouse1 = d3.mouse(this), between = rotateBetween(zoomPoint, position(projection, mouse1)); projection.rotate(eulerFromQuaternion(rotate = between ? multiply(rotate, between) : multiply(bank(projection, mouse0, mouse1), rotate))); mouse0 = mouse1; event.zoom.apply(this, arguments); }); event.zoomstart.apply(this, arguments); }) .on("zoomend", function() { zoomOn.call(zoom, "zoom", null); event.zoomend.apply(this, arguments); }), zoomOn = zoom.on; zoom.projection = function(_) { return arguments.length ? zoom.scale((projection = _).scale()) : projection; }; 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 t = projection.translate(), 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]; } 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] ]; } })();