toolkit/javascript/d3/d3.geo.js
changeset 47 c0b4a8b5a012
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/toolkit/javascript/d3/d3.geo.js	Thu Apr 10 14:20:23 2014 +0200
@@ -0,0 +1,931 @@
+(function(){d3.geo = {};
+
+var d3_geo_radians = Math.PI / 180;
+// TODO clip input coordinates on opposite hemisphere
+d3.geo.azimuthal = function() {
+  var mode = "orthographic", // or stereographic, gnomonic, equidistant or equalarea
+      origin,
+      scale = 200,
+      translate = [480, 250],
+      x0,
+      y0,
+      cy0,
+      sy0;
+
+  function azimuthal(coordinates) {
+    var x1 = coordinates[0] * d3_geo_radians - x0,
+        y1 = coordinates[1] * d3_geo_radians,
+        cx1 = Math.cos(x1),
+        sx1 = Math.sin(x1),
+        cy1 = Math.cos(y1),
+        sy1 = Math.sin(y1),
+        cc = mode !== "orthographic" ? sy0 * sy1 + cy0 * cy1 * cx1 : null,
+        c,
+        k = mode === "stereographic" ? 1 / (1 + cc)
+          : mode === "gnomonic" ? 1 / cc
+          : mode === "equidistant" ? (c = Math.acos(cc), c ? c / Math.sin(c) : 0)
+          : mode === "equalarea" ? Math.sqrt(2 / (1 + cc))
+          : 1,
+        x = k * cy1 * sx1,
+        y = k * (sy0 * cy1 * cx1 - cy0 * sy1);
+    return [
+      scale * x + translate[0],
+      scale * y + translate[1]
+    ];
+  }
+
+  azimuthal.invert = function(coordinates) {
+    var x = (coordinates[0] - translate[0]) / scale,
+        y = (coordinates[1] - translate[1]) / scale,
+        p = Math.sqrt(x * x + y * y),
+        c = mode === "stereographic" ? 2 * Math.atan(p)
+          : mode === "gnomonic" ? Math.atan(p)
+          : mode === "equidistant" ? p
+          : mode === "equalarea" ? 2 * Math.asin(.5 * p)
+          : Math.asin(p),
+        sc = Math.sin(c),
+        cc = Math.cos(c);
+    return [
+      (x0 + Math.atan2(x * sc, p * cy0 * cc + y * sy0 * sc)) / d3_geo_radians,
+      Math.asin(cc * sy0 - (p ? (y * sc * cy0) / p : 0)) / d3_geo_radians
+    ];
+  };
+
+  azimuthal.mode = function(x) {
+    if (!arguments.length) return mode;
+    mode = x + "";
+    return azimuthal;
+  };
+
+  azimuthal.origin = function(x) {
+    if (!arguments.length) return origin;
+    origin = x;
+    x0 = origin[0] * d3_geo_radians;
+    y0 = origin[1] * d3_geo_radians;
+    cy0 = Math.cos(y0);
+    sy0 = Math.sin(y0);
+    return azimuthal;
+  };
+
+  azimuthal.scale = function(x) {
+    if (!arguments.length) return scale;
+    scale = +x;
+    return azimuthal;
+  };
+
+  azimuthal.translate = function(x) {
+    if (!arguments.length) return translate;
+    translate = [+x[0], +x[1]];
+    return azimuthal;
+  };
+
+  return azimuthal.origin([0, 0]);
+};
+// Derived from Tom Carden's Albers implementation for Protovis.
+// http://gist.github.com/476238
+// http://mathworld.wolfram.com/AlbersEqual-AreaConicProjection.html
+
+d3.geo.albers = function() {
+  var origin = [-98, 38],
+      parallels = [29.5, 45.5],
+      scale = 1000,
+      translate = [480, 250],
+      lng0, // d3_geo_radians * origin[0]
+      n,
+      C,
+      p0;
+
+  function albers(coordinates) {
+    var t = n * (d3_geo_radians * coordinates[0] - lng0),
+        p = Math.sqrt(C - 2 * n * Math.sin(d3_geo_radians * coordinates[1])) / n;
+    return [
+      scale * p * Math.sin(t) + translate[0],
+      scale * (p * Math.cos(t) - p0) + translate[1]
+    ];
+  }
+
+  albers.invert = function(coordinates) {
+    var x = (coordinates[0] - translate[0]) / scale,
+        y = (coordinates[1] - translate[1]) / scale,
+        p0y = p0 + y,
+        t = Math.atan2(x, p0y),
+        p = Math.sqrt(x * x + p0y * p0y);
+    return [
+      (lng0 + t / n) / d3_geo_radians,
+      Math.asin((C - p * p * n * n) / (2 * n)) / d3_geo_radians
+    ];
+  };
+
+  function reload() {
+    var phi1 = d3_geo_radians * parallels[0],
+        phi2 = d3_geo_radians * parallels[1],
+        lat0 = d3_geo_radians * origin[1],
+        s = Math.sin(phi1),
+        c = Math.cos(phi1);
+    lng0 = d3_geo_radians * origin[0];
+    n = .5 * (s + Math.sin(phi2));
+    C = c * c + 2 * n * s;
+    p0 = Math.sqrt(C - 2 * n * Math.sin(lat0)) / n;
+    return albers;
+  }
+
+  albers.origin = function(x) {
+    if (!arguments.length) return origin;
+    origin = [+x[0], +x[1]];
+    return reload();
+  };
+
+  albers.parallels = function(x) {
+    if (!arguments.length) return parallels;
+    parallels = [+x[0], +x[1]];
+    return reload();
+  };
+
+  albers.scale = function(x) {
+    if (!arguments.length) return scale;
+    scale = +x;
+    return albers;
+  };
+
+  albers.translate = function(x) {
+    if (!arguments.length) return translate;
+    translate = [+x[0], +x[1]];
+    return albers;
+  };
+
+  return reload();
+};
+
+// A composite projection for the United States, 960x500. The set of standard
+// parallels for each region comes from USGS, which is published here:
+// http://egsc.usgs.gov/isb/pubs/MapProjections/projections.html#albers
+// TODO allow the composite projection to be rescaled?
+d3.geo.albersUsa = function() {
+  var lower48 = d3.geo.albers();
+
+  var alaska = d3.geo.albers()
+      .origin([-160, 60])
+      .parallels([55, 65]);
+
+  var hawaii = d3.geo.albers()
+      .origin([-160, 20])
+      .parallels([8, 18]);
+
+  var puertoRico = d3.geo.albers()
+      .origin([-60, 10])
+      .parallels([8, 18]);
+
+  function albersUsa(coordinates) {
+    var lon = coordinates[0],
+        lat = coordinates[1];
+    return (lat > 50 ? alaska
+        : lon < -140 ? hawaii
+        : lat < 21 ? puertoRico
+        : lower48)(coordinates);
+  }
+
+  albersUsa.scale = function(x) {
+    if (!arguments.length) return lower48.scale();
+    lower48.scale(x);
+    alaska.scale(x * .6);
+    hawaii.scale(x);
+    puertoRico.scale(x * 1.5);
+    return albersUsa.translate(lower48.translate());
+  };
+
+  albersUsa.translate = function(x) {
+    if (!arguments.length) return lower48.translate();
+    var dz = lower48.scale() / 1000,
+        dx = x[0],
+        dy = x[1];
+    lower48.translate(x);
+    alaska.translate([dx - 400 * dz, dy + 170 * dz]);
+    hawaii.translate([dx - 190 * dz, dy + 200 * dz]);
+    puertoRico.translate([dx + 580 * dz, dy + 430 * dz]);
+    return albersUsa;
+  };
+
+  return albersUsa.scale(lower48.scale());
+};
+d3.geo.bonne = function() {
+  var scale = 200,
+      translate = [480, 250],
+      x0, // origin longitude in radians
+      y0, // origin latitude in radians
+      y1, // parallel latitude in radians
+      c1; // cot(y1)
+
+  function bonne(coordinates) {
+    var x = coordinates[0] * d3_geo_radians - x0,
+        y = coordinates[1] * d3_geo_radians - y0;
+    if (y1) {
+      var p = c1 + y1 - y, E = x * Math.cos(y) / p;
+      x = p * Math.sin(E);
+      y = p * Math.cos(E) - c1;
+    } else {
+      x *= Math.cos(y);
+      y *= -1;
+    }
+    return [
+      scale * x + translate[0],
+      scale * y + translate[1]
+    ];
+  }
+
+  bonne.invert = function(coordinates) {
+    var x = (coordinates[0] - translate[0]) / scale,
+        y = (coordinates[1] - translate[1]) / scale;
+    if (y1) {
+      var c = c1 + y, p = Math.sqrt(x * x + c * c);
+      y = c1 + y1 - p;
+      x = x0 + p * Math.atan2(x, c) / Math.cos(y);
+    } else {
+      y *= -1;
+      x /= Math.cos(y);
+    }
+    return [
+      x / d3_geo_radians,
+      y / d3_geo_radians
+    ];
+  };
+
+  // 90° for Werner, 0° for Sinusoidal
+  bonne.parallel = function(x) {
+    if (!arguments.length) return y1 / d3_geo_radians;
+    c1 = 1 / Math.tan(y1 = x * d3_geo_radians);
+    return bonne;
+  };
+
+  bonne.origin = function(x) {
+    if (!arguments.length) return [x0 / d3_geo_radians, y0 / d3_geo_radians];
+    x0 = x[0] * d3_geo_radians;
+    y0 = x[1] * d3_geo_radians;
+    return bonne;
+  };
+
+  bonne.scale = function(x) {
+    if (!arguments.length) return scale;
+    scale = +x;
+    return bonne;
+  };
+
+  bonne.translate = function(x) {
+    if (!arguments.length) return translate;
+    translate = [+x[0], +x[1]];
+    return bonne;
+  };
+
+  return bonne.origin([0, 0]).parallel(45);
+};
+d3.geo.equirectangular = function() {
+  var scale = 500,
+      translate = [480, 250];
+
+  function equirectangular(coordinates) {
+    var x = coordinates[0] / 360,
+        y = -coordinates[1] / 360;
+    return [
+      scale * x + translate[0],
+      scale * y + translate[1]
+    ];
+  }
+
+  equirectangular.invert = function(coordinates) {
+    var x = (coordinates[0] - translate[0]) / scale,
+        y = (coordinates[1] - translate[1]) / scale;
+    return [
+      360 * x,
+      -360 * y
+    ];
+  };
+
+  equirectangular.scale = function(x) {
+    if (!arguments.length) return scale;
+    scale = +x;
+    return equirectangular;
+  };
+
+  equirectangular.translate = function(x) {
+    if (!arguments.length) return translate;
+    translate = [+x[0], +x[1]];
+    return equirectangular;
+  };
+
+  return equirectangular;
+};
+d3.geo.mercator = function() {
+  var scale = 500,
+      translate = [480, 250];
+
+  function mercator(coordinates) {
+    var x = coordinates[0] / 360,
+        y = -(Math.log(Math.tan(Math.PI / 4 + coordinates[1] * d3_geo_radians / 2)) / d3_geo_radians) / 360;
+    return [
+      scale * x + translate[0],
+      scale * Math.max(-.5, Math.min(.5, y)) + translate[1]
+    ];
+  }
+
+  mercator.invert = function(coordinates) {
+    var x = (coordinates[0] - translate[0]) / scale,
+        y = (coordinates[1] - translate[1]) / scale;
+    return [
+      360 * x,
+      2 * Math.atan(Math.exp(-360 * y * d3_geo_radians)) / d3_geo_radians - 90
+    ];
+  };
+
+  mercator.scale = function(x) {
+    if (!arguments.length) return scale;
+    scale = +x;
+    return mercator;
+  };
+
+  mercator.translate = function(x) {
+    if (!arguments.length) return translate;
+    translate = [+x[0], +x[1]];
+    return mercator;
+  };
+
+  return mercator;
+};
+function d3_geo_type(types, defaultValue) {
+  return function(object) {
+    return object && object.type in types ? types[object.type](object) : defaultValue;
+  };
+}
+/**
+ * Returns a function that, given a GeoJSON object (e.g., a feature), returns
+ * the corresponding SVG path. The function can be customized by overriding the
+ * projection. Point features are mapped to circles with a default radius of
+ * 4.5px; the radius can be specified either as a constant or a function that
+ * is evaluated per object.
+ */
+d3.geo.path = function() {
+  var pointRadius = 4.5,
+      pointCircle = d3_path_circle(pointRadius),
+      projection = d3.geo.albersUsa();
+
+  function path(d, i) {
+    if (typeof pointRadius === "function") {
+      pointCircle = d3_path_circle(pointRadius.apply(this, arguments));
+    }
+    return pathType(d) || null;
+  }
+
+  function project(coordinates) {
+    return projection(coordinates).join(",");
+  }
+
+  var pathType = d3_geo_type({
+
+    FeatureCollection: function(o) {
+      var path = [],
+          features = o.features,
+          i = -1, // features.index
+          n = features.length;
+      while (++i < n) path.push(pathType(features[i].geometry));
+      return path.join("");
+    },
+
+    Feature: function(o) {
+      return pathType(o.geometry);
+    },
+
+    Point: function(o) {
+      return "M" + project(o.coordinates) + pointCircle;
+    },
+
+    MultiPoint: function(o) {
+      var path = [],
+          coordinates = o.coordinates,
+          i = -1, // coordinates.index
+          n = coordinates.length;
+      while (++i < n) path.push("M", project(coordinates[i]), pointCircle);
+      return path.join("");
+    },
+
+    LineString: function(o) {
+      var path = ["M"],
+          coordinates = o.coordinates,
+          i = -1, // coordinates.index
+          n = coordinates.length;
+      while (++i < n) path.push(project(coordinates[i]), "L");
+      path.pop();
+      return path.join("");
+    },
+
+    MultiLineString: function(o) {
+      var path = [],
+          coordinates = o.coordinates,
+          i = -1, // coordinates.index
+          n = coordinates.length,
+          subcoordinates, // coordinates[i]
+          j, // subcoordinates.index
+          m; // subcoordinates.length
+      while (++i < n) {
+        subcoordinates = coordinates[i];
+        j = -1;
+        m = subcoordinates.length;
+        path.push("M");
+        while (++j < m) path.push(project(subcoordinates[j]), "L");
+        path.pop();
+      }
+      return path.join("");
+    },
+
+    Polygon: function(o) {
+      var path = [],
+          coordinates = o.coordinates,
+          i = -1, // coordinates.index
+          n = coordinates.length,
+          subcoordinates, // coordinates[i]
+          j, // subcoordinates.index
+          m; // subcoordinates.length
+      while (++i < n) {
+        subcoordinates = coordinates[i];
+        j = -1;
+        if ((m = subcoordinates.length - 1) > 0) {
+          path.push("M");
+          while (++j < m) path.push(project(subcoordinates[j]), "L");
+          path[path.length - 1] = "Z";
+        }
+      }
+      return path.join("");
+    },
+
+    MultiPolygon: function(o) {
+      var path = [],
+          coordinates = o.coordinates,
+          i = -1, // coordinates index
+          n = coordinates.length,
+          subcoordinates, // coordinates[i]
+          j, // subcoordinates index
+          m, // subcoordinates.length
+          subsubcoordinates, // subcoordinates[j]
+          k, // subsubcoordinates index
+          p; // subsubcoordinates.length
+      while (++i < n) {
+        subcoordinates = coordinates[i];
+        j = -1;
+        m = subcoordinates.length;
+        while (++j < m) {
+          subsubcoordinates = subcoordinates[j];
+          k = -1;
+          if ((p = subsubcoordinates.length - 1) > 0) {
+            path.push("M");
+            while (++k < p) path.push(project(subsubcoordinates[k]), "L");
+            path[path.length - 1] = "Z";
+          }
+        }
+      }
+      return path.join("");
+    },
+
+    GeometryCollection: function(o) {
+      var path = [],
+          geometries = o.geometries,
+          i = -1, // geometries index
+          n = geometries.length;
+      while (++i < n) path.push(pathType(geometries[i]));
+      return path.join("");
+    }
+
+  });
+
+  var areaType = path.area = d3_geo_type({
+
+    FeatureCollection: function(o) {
+      var area = 0,
+          features = o.features,
+          i = -1, // features.index
+          n = features.length;
+      while (++i < n) area += areaType(features[i]);
+      return area;
+    },
+
+    Feature: function(o) {
+      return areaType(o.geometry);
+    },
+
+    Polygon: function(o) {
+      return polygonArea(o.coordinates);
+    },
+
+    MultiPolygon: function(o) {
+      var sum = 0,
+          coordinates = o.coordinates,
+          i = -1, // coordinates index
+          n = coordinates.length;
+      while (++i < n) sum += polygonArea(coordinates[i]);
+      return sum;
+    },
+
+    GeometryCollection: function(o) {
+      var sum = 0,
+          geometries = o.geometries,
+          i = -1, // geometries index
+          n = geometries.length;
+      while (++i < n) sum += areaType(geometries[i]);
+      return sum;
+    }
+
+  }, 0);
+
+  function polygonArea(coordinates) {
+    var sum = area(coordinates[0]), // exterior ring
+        i = 0, // coordinates.index
+        n = coordinates.length;
+    while (++i < n) sum -= area(coordinates[i]); // holes
+    return sum;
+  }
+
+  function polygonCentroid(coordinates) {
+    var polygon = d3.geom.polygon(coordinates[0].map(projection)), // exterior ring
+        area = polygon.area(),
+        centroid = polygon.centroid(area < 0 ? (area *= -1, 1) : -1),
+        x = centroid[0],
+        y = centroid[1],
+        z = area,
+        i = 0, // coordinates index
+        n = coordinates.length;
+    while (++i < n) {
+      polygon = d3.geom.polygon(coordinates[i].map(projection)); // holes
+      area = polygon.area();
+      centroid = polygon.centroid(area < 0 ? (area *= -1, 1) : -1);
+      x -= centroid[0];
+      y -= centroid[1];
+      z -= area;
+    }
+    return [x, y, 6 * z]; // weighted centroid
+  }
+
+  var centroidType = path.centroid = d3_geo_type({
+
+    // TODO FeatureCollection
+    // TODO Point
+    // TODO MultiPoint
+    // TODO LineString
+    // TODO MultiLineString
+    // TODO GeometryCollection
+
+    Feature: function(o) {
+      return centroidType(o.geometry);
+    },
+
+    Polygon: function(o) {
+      var centroid = polygonCentroid(o.coordinates);
+      return [centroid[0] / centroid[2], centroid[1] / centroid[2]];
+    },
+
+    MultiPolygon: function(o) {
+      var area = 0,
+          coordinates = o.coordinates,
+          centroid,
+          x = 0,
+          y = 0,
+          z = 0,
+          i = -1, // coordinates index
+          n = coordinates.length;
+      while (++i < n) {
+        centroid = polygonCentroid(coordinates[i]);
+        x += centroid[0];
+        y += centroid[1];
+        z += centroid[2];
+      }
+      return [x / z, y / z];
+    }
+
+  });
+
+  function area(coordinates) {
+    return Math.abs(d3.geom.polygon(coordinates.map(projection)).area());
+  }
+
+  path.projection = function(x) {
+    projection = x;
+    return path;
+  };
+
+  path.pointRadius = function(x) {
+    if (typeof x === "function") pointRadius = x;
+    else {
+      pointRadius = +x;
+      pointCircle = d3_path_circle(pointRadius);
+    }
+    return path;
+  };
+
+  return path;
+};
+
+function d3_path_circle(radius) {
+  return "m0," + radius
+      + "a" + radius + "," + radius + " 0 1,1 0," + (-2 * radius)
+      + "a" + radius + "," + radius + " 0 1,1 0," + (+2 * radius)
+      + "z";
+}
+/**
+ * Given a GeoJSON object, returns the corresponding bounding box. The bounding
+ * box is represented by a two-dimensional array: [[left, bottom], [right,
+ * top]], where left is the minimum longitude, bottom is the minimum latitude,
+ * right is maximum longitude, and top is the maximum latitude.
+ */
+d3.geo.bounds = function(feature) {
+  var left = Infinity,
+      bottom = Infinity,
+      right = -Infinity,
+      top = -Infinity;
+  d3_geo_bounds(feature, function(x, y) {
+    if (x < left) left = x;
+    if (x > right) right = x;
+    if (y < bottom) bottom = y;
+    if (y > top) top = y;
+  });
+  return [[left, bottom], [right, top]];
+};
+
+function d3_geo_bounds(o, f) {
+  if (o.type in d3_geo_boundsTypes) d3_geo_boundsTypes[o.type](o, f);
+}
+
+var d3_geo_boundsTypes = {
+  Feature: d3_geo_boundsFeature,
+  FeatureCollection: d3_geo_boundsFeatureCollection,
+  LineString: d3_geo_boundsLineString,
+  MultiLineString: d3_geo_boundsMultiLineString,
+  MultiPoint: d3_geo_boundsLineString,
+  MultiPolygon: d3_geo_boundsMultiPolygon,
+  Point: d3_geo_boundsPoint,
+  Polygon: d3_geo_boundsPolygon
+};
+
+function d3_geo_boundsFeature(o, f) {
+  d3_geo_bounds(o.geometry, f);
+}
+
+function d3_geo_boundsFeatureCollection(o, f) {
+  for (var a = o.features, i = 0, n = a.length; i < n; i++) {
+    d3_geo_bounds(a[i].geometry, f);
+  }
+}
+
+function d3_geo_boundsLineString(o, f) {
+  for (var a = o.coordinates, i = 0, n = a.length; i < n; i++) {
+    f.apply(null, a[i]);
+  }
+}
+
+function d3_geo_boundsMultiLineString(o, f) {
+  for (var a = o.coordinates, i = 0, n = a.length; i < n; i++) {
+    for (var b = a[i], j = 0, m = b.length; j < m; j++) {
+      f.apply(null, b[j]);
+    }
+  }
+}
+
+function d3_geo_boundsMultiPolygon(o, f) {
+  for (var a = o.coordinates, i = 0, n = a.length; i < n; i++) {
+    for (var b = a[i][0], j = 0, m = b.length; j < m; j++) {
+      f.apply(null, b[j]);
+    }
+  }
+}
+
+function d3_geo_boundsPoint(o, f) {
+  f.apply(null, o.coordinates);
+}
+
+function d3_geo_boundsPolygon(o, f) {
+  for (var a = o.coordinates[0], i = 0, n = a.length; i < n; i++) {
+    f.apply(null, a[i]);
+  }
+}
+// TODO breakAtDateLine?
+
+d3.geo.circle = function() {
+  var origin = [0, 0],
+      degrees = 90 - 1e-2,
+      radians = degrees * d3_geo_radians,
+      arc = d3.geo.greatArc().target(Object);
+
+  function circle() {
+    // TODO render a circle as a Polygon
+  }
+
+  function visible(point) {
+    return arc.distance(point) < radians;
+  }
+
+  circle.clip = function(d) {
+    arc.source(typeof origin === "function" ? origin.apply(this, arguments) : origin);
+    return clipType(d);
+  };
+
+  var clipType = d3_geo_type({
+
+    FeatureCollection: function(o) {
+      var features = o.features.map(clipType).filter(Object);
+      return features && (o = Object.create(o), o.features = features, o);
+    },
+
+    Feature: function(o) {
+      var geometry = clipType(o.geometry);
+      return geometry && (o = Object.create(o), o.geometry = geometry, o);
+    },
+
+    Point: function(o) {
+      return visible(o.coordinates) && o;
+    },
+
+    MultiPoint: function(o) {
+      var coordinates = o.coordinates.filter(visible);
+      return coordinates.length && {
+        type: o.type,
+        coordinates: coordinates
+      };
+    },
+
+    LineString: function(o) {
+      var coordinates = clip(o.coordinates);
+      return coordinates.length && (o = Object.create(o), o.coordinates = coordinates, o);
+    },
+
+    MultiLineString: function(o) {
+      var coordinates = o.coordinates.map(clip).filter(function(d) { return d.length; });
+      return coordinates.length && (o = Object.create(o), o.coordinates = coordinates, o);
+    },
+
+    Polygon: function(o) {
+      var coordinates = o.coordinates.map(clip);
+      return coordinates[0].length && (o = Object.create(o), o.coordinates = coordinates, o);
+    },
+
+    MultiPolygon: function(o) {
+      var coordinates = o.coordinates.map(function(d) { return d.map(clip); }).filter(function(d) { return d[0].length; });
+      return coordinates.length && (o = Object.create(o), o.coordinates = coordinates, o);
+    },
+
+    GeometryCollection: function(o) {
+      var geometries = o.geometries.map(clipType).filter(Object);
+      return geometries.length && (o = Object.create(o), o.geometries = geometries, o);
+    }
+
+  });
+
+  function clip(coordinates) {
+    var i = -1,
+        n = coordinates.length,
+        clipped = [],
+        p0,
+        p1,
+        p2,
+        d0,
+        d1;
+
+    while (++i < n) {
+      d1 = arc.distance(p2 = coordinates[i]);
+      if (d1 < radians) {
+        if (p1) clipped.push(d3_geo_greatArcInterpolate(p1, p2)((d0 - radians) / (d0 - d1)));
+        clipped.push(p2);
+        p0 = p1 = null;
+      } else {
+        p1 = p2;
+        if (!p0 && clipped.length) {
+          clipped.push(d3_geo_greatArcInterpolate(clipped[clipped.length - 1], p1)((radians - d0) / (d1 - d0)));
+          p0 = p1;
+        }
+      }
+      d0 = d1;
+    }
+
+    if (p1 && clipped.length) {
+      d1 = arc.distance(p2 = clipped[0]);
+      clipped.push(d3_geo_greatArcInterpolate(p1, p2)((d0 - radians) / (d0 - d1)));
+    }
+
+    return resample(clipped);
+  }
+
+  // Resample coordinates, creating great arcs between each.
+  function resample(coordinates) {
+    var i = 0,
+        n = coordinates.length,
+        j,
+        m,
+        resampled = n ? [coordinates[0]] : coordinates,
+        resamples,
+        origin = arc.source();
+
+    while (++i < n) {
+      resamples = arc.source(coordinates[i - 1])(coordinates[i]).coordinates;
+      for (j = 0, m = resamples.length; ++j < m;) resampled.push(resamples[j]);
+    }
+
+    arc.source(origin);
+    return resampled;
+  }
+
+  circle.origin = function(x) {
+    if (!arguments.length) return origin;
+    origin = x;
+    return circle;
+  };
+
+  circle.angle = function(x) {
+    if (!arguments.length) return degrees;
+    radians = (degrees = +x) * d3_geo_radians;
+    return circle;
+  };
+
+  // Precision is specified in degrees.
+  circle.precision = function(x) {
+    if (!arguments.length) return arc.precision();
+    arc.precision(x);
+    return circle;
+  };
+
+  return circle;
+}
+d3.geo.greatArc = function() {
+  var source = d3_geo_greatArcSource,
+      target = d3_geo_greatArcTarget,
+      precision = 6 * d3_geo_radians;
+
+  function greatArc() {
+    var a = typeof source === "function" ? source.apply(this, arguments) : source,
+        b = typeof target === "function" ? target.apply(this, arguments) : target,
+        i = d3_geo_greatArcInterpolate(a, b),
+        dt = precision / i.d,
+        t = 0,
+        coordinates = [a];
+    while ((t += dt) < 1) coordinates.push(i(t));
+    coordinates.push(b);
+    return {
+      type: "LineString",
+      coordinates: coordinates
+    };
+  }
+
+  // Length returned in radians; multiply by radius for distance.
+  greatArc.distance = function() {
+    var a = typeof source === "function" ? source.apply(this, arguments) : source,
+        b = typeof target === "function" ? target.apply(this, arguments) : target;
+     return d3_geo_greatArcInterpolate(a, b).d;
+  };
+
+  greatArc.source = function(x) {
+    if (!arguments.length) return source;
+    source = x;
+    return greatArc;
+  };
+
+  greatArc.target = function(x) {
+    if (!arguments.length) return target;
+    target = x;
+    return greatArc;
+  };
+
+  // Precision is specified in degrees.
+  greatArc.precision = function(x) {
+    if (!arguments.length) return precision / d3_geo_radians;
+    precision = x * d3_geo_radians;
+    return greatArc;
+  };
+
+  return greatArc;
+};
+
+function d3_geo_greatArcSource(d) {
+  return d.source;
+}
+
+function d3_geo_greatArcTarget(d) {
+  return d.target;
+}
+
+function d3_geo_greatArcInterpolate(a, b) {
+  var x0 = a[0] * d3_geo_radians, cx0 = Math.cos(x0), sx0 = Math.sin(x0),
+      y0 = a[1] * d3_geo_radians, cy0 = Math.cos(y0), sy0 = Math.sin(y0),
+      x1 = b[0] * d3_geo_radians, cx1 = Math.cos(x1), sx1 = Math.sin(x1),
+      y1 = b[1] * d3_geo_radians, cy1 = Math.cos(y1), sy1 = Math.sin(y1),
+      d = interpolate.d = Math.acos(Math.max(-1, Math.min(1, sy0 * sy1 + cy0 * cy1 * Math.cos(x1 - x0)))),
+      sd = Math.sin(d);
+
+  // From http://williams.best.vwh.net/avform.htm#Intermediate
+  function interpolate(t) {
+    var A = Math.sin(d - (t *= d)) / sd,
+        B = Math.sin(t) / sd,
+        x = A * cy0 * cx0 + B * cy1 * cx1,
+        y = A * cy0 * sx0 + B * cy1 * sx1,
+        z = A * sy0       + B * sy1;
+    return [
+      Math.atan2(y, x) / d3_geo_radians,
+      Math.atan2(z, Math.sqrt(x * x + y * y)) / d3_geo_radians
+    ];
+  }
+
+  return interpolate;
+}
+d3.geo.greatCircle = d3.geo.circle;
+})();