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