Skip to content

Commit

Permalink
Add d3.geo.centroid.
Browse files Browse the repository at this point in the history
Polygons are temporarily treated as lines while I work on adding area
weighting.

See #941.
  • Loading branch information
jasondavies authored and mbostock committed Dec 9, 2012
1 parent 560a091 commit 2467834
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 27 deletions.
1 change: 1 addition & 0 deletions Makefile
Expand Up @@ -186,6 +186,7 @@ d3.geo.js: \
src/geo/azimuthal-equal-area.js \
src/geo/azimuthal-equidistant.js \
src/geo/bounds.js \
src/geo/centroid.js \
src/geo/circle.js \
src/geo/compose.js \
src/geo/equirectangular.js \
Expand Down
88 changes: 77 additions & 11 deletions d3.js
Expand Up @@ -5501,6 +5501,83 @@
return [ [ x0, y0 ], [ x1, y1 ] ];
};
}
d3.geo.centroid = function(object) {
return d3_geo_centroidType.object(object);
};
var d3_geo_centroidType = d3_geo_type({
Feature: function(feature) {
return this.geometry(feature.geometry);
},
Point: function(point) {
return point.coordinates;
},
MultiPoint: function(multiPoint) {
var coordinates = multiPoint.coordinates, n = coordinates.length, i = 0, point, λ, φ, cosφ, x = 0, y = 0, z = 0;
while (i < n) {
point = coordinates[i++];
λ = point[0] * d3_radians;
cosφ = Math.cos(φ = point[1] * d3_radians);
x += (cosφ * Math.cos(λ) - x) / i;
y += (cosφ * Math.sin(λ) - y) / i;
z += (Math.sin(φ) - z) / i;
}
return n ? [ Math.atan2(y, x) * d3_degrees, Math.asin(Math.max(-1, Math.min(1, z))) * d3_degrees ] : null;
},
LineString: function(line) {
var centroid = d3_geo_centroidLine(line.coordinates), cx, cy, cz;
return centroid && centroid[3] ? [ Math.atan2(cy = centroid[1], cx = centroid[0]) * d3_degrees, Math.asin(Math.max(-1, Math.min(1, (cz = centroid[2]) / Math.sqrt(cx * cx + cy * cy + cz * cz)))) * d3_degrees ] : null;
},
MultiLineString: function(multiLine) {
var coordinates = multiLine.coordinates, n = coordinates.length, i = -1, cx = 0, cy = 0, cz = 0, weight = 0;
while (++i < n) {
centroid = d3_geo_centroidLine(coordinates[i]);
if (!centroid) continue;
cx += centroid[0];
cy += centroid[1];
cz += centroid[2];
weight += centroid[3];
}
return weight ? [ Math.atan2(cy, cx) * d3_degrees, Math.asin(Math.max(-1, Math.min(1, cz / Math.sqrt(cx * cx + cy * cy + cz * cz)))) * d3_degrees ] : null;
},
Polygon: function(polygon) {
return this.MultiLineString(polygon);
},
MultiPolygon: function(multiPolygon) {
var coordinates = multiPolygon.coordinates, n = coordinates.length, i = -1, cx = 0, cy = 0, cz = 0, weight = 0, polygon;
while (++i < n) {
polygon = coordinates[i];
for (var j = 0, m = polygon.length; j < m; ++j) {
centroid = d3_geo_centroidLine(polygon[j]);
if (!centroid) continue;
cx += centroid[0];
cy += centroid[1];
cz += centroid[2];
weight += centroid[3];
}
}
return weight ? [ Math.atan2(cy, cx) * d3_degrees, Math.asin(Math.max(-1, Math.min(1, cz / Math.sqrt(cx * cx + cy * cy + cz * cz)))) * d3_degrees ] : null;
}
});
function d3_geo_centroidLine(coordinates) {
if (!(n = coordinates.length)) return null;
var point = coordinates[0], i = 0, n, λ = point[0] * d3_radians, φ, cosφ = Math.cos(φ = point[1] * d3_radians), x0 = cosφ * Math.cos(λ), y0 = cosφ * Math.sin(λ), z0 = Math.sin(φ), cx = 0, cy = 0, cz = 0, x, y, z, w, weight = 0;
while (++i < n) {
point = coordinates[i];
λ = point[0] * d3_radians;
cosφ = Math.cos(φ = point[1] * d3_radians);
x = cosφ * Math.cos(λ);
y = cosφ * Math.sin(λ);
z = Math.sin(φ);
weight += w = Math.atan2(Math.sqrt((w = y0 * z - z0 * y) * w + (w = z0 * x - x0 * z) * w + (w = x0 * y - y0 * x) * w), x0 * x + y0 * y + z0 * z);
x = (x0 + (x0 = x)) / 2;
y = (y0 + (y0 = y)) / 2;
z = (z0 + (z0 = z)) / 2;
cx += w * x;
cy += w * y;
cz += w * z;
}
return [ cx, cy, cz, weight ];
}
d3.geo.circle = function() {
var origin = [ 0, 0 ], angle, precision = 6, rotate, interpolate;
function circle() {
Expand Down Expand Up @@ -6144,17 +6221,6 @@
function d3_geo_pathCircle(radius) {
return "m0," + radius + "a" + radius + "," + radius + " 0 1,1 0," + -2 * radius + "a" + radius + "," + radius + " 0 1,1 0," + +2 * radius + "z";
}
var d3_geo_pathIdentity = d3.geo.path().projection({
polygon: function(polygon, context) {
polygon.forEach(function(ring) {
var n = ring.length, i = 0, point;
context.moveTo((point = ring[0])[0], point[1]);
while (++i < n) context.lineTo((point = ring[i])[0], point[1]);
context.closePath();
});
}
});
d3.geo.centroid = d3_geo_pathIdentity.centroid;
d3.geo.projection = d3_geo_projection;
d3.geo.projectionMutator = d3_geo_projectionMutator;
function d3_geo_projection(project) {
Expand Down
8 changes: 4 additions & 4 deletions d3.min.js

Large diffs are not rendered by default.

133 changes: 132 additions & 1 deletion src/geo/centroid.js
@@ -1 +1,132 @@
d3.geo.centroid = d3_geo_pathIdentity.centroid;
d3.geo.centroid = function(object) {
return d3_geo_centroidType.object(object);
};

var d3_geo_centroidType = d3_geo_type({
Feature: function(feature) { return this.geometry(feature.geometry); },
Point: function(point) { return point.coordinates; },
MultiPoint: function(multiPoint) {
var coordinates = multiPoint.coordinates,
n = coordinates.length,
i = 0,
point,
λ,
φ,
cosφ,
x = 0,
y = 0,
z = 0;
while (i < n) {
point = coordinates[i++];
λ = point[0] * d3_radians;
cosφ = Math.cos(φ = point[1] * d3_radians);
x += (cosφ * Math.cos(λ) - x) / i;
y += (cosφ * Math.sin(λ) - y) / i;
z += (Math.sin(φ) - z) / i;
}
return n ? [
Math.atan2(y, x) * d3_degrees,
Math.asin(Math.max(-1, Math.min(1, z))) * d3_degrees
] : null;
},
LineString: function(line) {
var centroid = d3_geo_centroidLine(line.coordinates),
cx,
cy,
cz;
return centroid && centroid[3] ? [
Math.atan2(cy = centroid[1], cx = centroid[0]) * d3_degrees,
Math.asin(Math.max(-1, Math.min(1, (cz = centroid[2]) / Math.sqrt(cx * cx + cy * cy + cz * cz)))) * d3_degrees
] : null;
},
MultiLineString: function(multiLine) {
var coordinates = multiLine.coordinates,
n = coordinates.length,
i = -1,
cx = 0,
cy = 0,
cz = 0,
weight = 0;
while (++i < n) {
centroid = d3_geo_centroidLine(coordinates[i]);
if (!centroid) continue;
cx += centroid[0];
cy += centroid[1];
cz += centroid[2];
weight += centroid[3];
}
return weight ? [
Math.atan2(cy, cx) * d3_degrees,
Math.asin(Math.max(-1, Math.min(1, cz / Math.sqrt(cx * cx + cy * cy + cz * cz)))) * d3_degrees
] : null;
},
Polygon: function(polygon) {
// TODO use area weighting
return this.MultiLineString(polygon);
},
MultiPolygon: function(multiPolygon) {
// TODO use area weighting
var coordinates = multiPolygon.coordinates,
n = coordinates.length,
i = -1,
cx = 0,
cy = 0,
cz = 0,
weight = 0,
polygon;
while (++i < n) {
polygon = coordinates[i];
for (var j = 0, m = polygon.length; j < m; ++j) {
centroid = d3_geo_centroidLine(polygon[j]);
if (!centroid) continue;
cx += centroid[0];
cy += centroid[1];
cz += centroid[2];
weight += centroid[3];
}
}
return weight ? [
Math.atan2(cy, cx) * d3_degrees,
Math.asin(Math.max(-1, Math.min(1, cz / Math.sqrt(cx * cx + cy * cy + cz * cz)))) * d3_degrees
] : null;
}
});

function d3_geo_centroidLine(coordinates) {
if (!(n = coordinates.length)) return null;
var point = coordinates[0],
i = 0,
n,
λ = point[0] * d3_radians,
φ,
cosφ = Math.cos(φ = point[1] * d3_radians),
x0 = cosφ * Math.cos(λ),
y0 = cosφ * Math.sin(λ),
z0 = Math.sin(φ),
cx = 0,
cy = 0,
cz = 0,
x,
y,
z,
w,
weight = 0;
while (++i < n) {
point = coordinates[i];
λ = point[0] * d3_radians;
cosφ = Math.cos(φ = point[1] * d3_radians);
x = cosφ * Math.cos(λ);
y = cosφ * Math.sin(λ);
z = Math.sin(φ);
weight += w = Math.atan2(
Math.sqrt((w = y0 * z - z0 * y) * w + (w = z0 * x - x0 * z) * w + (w = x0 * y - y0 * x) * w),
x0 * x + y0 * y + z0 * z);
x = (x0 + (x0 = x)) / 2;
y = (y0 + (y0 = y)) / 2;
z = (z0 + (z0 = z)) / 2;
cx += w * x;
cy += w * y;
cz += w * z;
}
return [cx, cy, cz, weight];
}
11 changes: 0 additions & 11 deletions src/geo/path.js
Expand Up @@ -185,14 +185,3 @@ function d3_geo_pathCircle(radius) {
+ "a" + radius + "," + radius + " 0 1,1 0," + (+2 * radius)
+ "z";
}

var d3_geo_pathIdentity = d3.geo.path().projection({
polygon: function(polygon, context) {
polygon.forEach(function(ring) {
var n = ring.length, i = 0, point;
context.moveTo((point = ring[0])[0], point[1]);
while (++i < n) context.lineTo((point = ring[i])[0], point[1]);
context.closePath();
});
}
});
58 changes: 58 additions & 0 deletions test/geo/centroid-test.js
@@ -0,0 +1,58 @@
require("../env");

var vows = require("vows"),
assert = require("assert");

var suite = vows.describe("d3.geo.centroid");

suite.addBatch({
"centroid": {
topic: function() {
return d3.geo.centroid;
},
"Point": function(centroid) {
assert.deepEqual(centroid({type: "Point", coordinates: [0, 0]}), [0, 0]);
},
"MultiPoint": function(centroid) {
assert.inDelta(centroid({type: "MultiPoint", coordinates: [[0, 0], [1, 2]]}), [0.499847, 0.999847], 1e-6);
assert.deepEqual(centroid({type: "MultiPoint", coordinates: [[179, 0], [-179, 0]]}), [180, 0]);
},
"LineString": function(centroid) {
assert.inDelta(centroid({type: "LineString", coordinates: [[0, 0], [1, 0]]}), [.5, 0], 1e-6);
assert.inDelta(centroid({type: "LineString", coordinates: [[0, 0], [0, 90]]}), [0, 45], 1e-6);
assert.inDelta(centroid({type: "LineString", coordinates: [[0, 0], [0, 45], [0, 90]]}), [0, 45], 1e-6);
assert.inDelta(centroid({type: "LineString", coordinates: [[-1, -1], [1, 1]]}), [0, 0], 1e-6);
assert.inDelta(centroid({type: "LineString", coordinates: [[-60, -1], [60, 1]]}), [0, 0], 1e-6);
assert.inDelta(centroid({type: "LineString", coordinates: [[179, -1], [-179, 1]]}), [180, 0], 1e-6);
assert.inDelta(centroid({type: "LineString", coordinates: [[-179, 0], [0, 0], [179, 0]]}), [0, 0], 1e-6);
assert.inDelta(centroid({type: "LineString", coordinates: [[0, -90], [0, 90]]}), [0, 0], 1e-6);
assert.inDelta(centroid({type: "LineString", coordinates: [[-180, -90], [0, 90]]}), [-90, 0], 1e-6);
},
"MultiLineString": function(centroid) {
assert.inDelta(centroid({type: "MultiLineString", coordinates: [[[0, 0], [0, 2]]]}), [0, 1], 1e-6);
},
"Polygon": function(centroid) {
assert.inDelta(centroid({type: "Polygon", coordinates: [[[0, -90], [0, 0], [0, 90], [1, 0], [0, -90]]]}), [.5, 0], 1e-6);
assert.inDelta(centroid(d3.geo.circle().angle(5).origin([0, 45])()), [0, 45], 1e-6);
assert.equal(centroid({type: "Polygon", coordinates: [d3.range(-180, 180 + 1 / 2, 1).map(function(x) { return [x, -60]; })]})[1], -90);
assert.inDelta(centroid({type: "Polygon", coordinates: [[[0, -10], [0, 10], [10, 10], [10, -10], [0, -10]]]}), [5, 0], 1e-6);
},
"MultiPolygon": function(centroid) {
assert.inDelta(centroid({type: "MultiPolygon", coordinates: [[[[0, -90], [0, 0], [0, 90], [1, 0], [0, -90]]]]}), [.5, 0], 1e-6);
},
"Sphere": function(centroid) {
assert.isUndefined(centroid({type: "Sphere"}));
},
"Feature": function(centroid) {
assert.deepEqual(centroid({type: "Feature", geometry: {type: "Point", coordinates: [0, 0]}}), [0, 0]);
},
"FeatureCollection": function(centroid) {
assert.isUndefined(centroid({type: "FeatureCollection", features: [{type: "Feature", geometry: {type: "Point", coordinates: [0, 0]}}]}));
},
"GeometryCollection": function(centroid) {
assert.isUndefined(centroid({type: "GeometryCollection", geometries: [{type: "Point", coordinates: [0, 0]}]}));
}
}
});

suite.export(module);

0 comments on commit 2467834

Please sign in to comment.