Merge branch 'better-hull2' of https://github.com/DanGoldbach/d3 into 3.4
This commit is contained in:
Коммит
d0a047ae80
|
@ -1179,6 +1179,12 @@ d3 = function() {
|
|||
function d3_sgn(x) {
|
||||
return x > 0 ? 1 : x < 0 ? -1 : 0;
|
||||
}
|
||||
function d3_isCCWTurn(a, b, c) {
|
||||
return d3_cross2d(a, b, c) > 0;
|
||||
}
|
||||
function d3_cross2d(o, a, b) {
|
||||
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
|
||||
}
|
||||
function d3_acos(x) {
|
||||
return x > 1 ? 0 : x < -1 ? π : Math.acos(x);
|
||||
}
|
||||
|
@ -3117,18 +3123,15 @@ d3 = function() {
|
|||
for (var j = 1, v = polygon[i], m = v.length, a = v[0], b; j < m; ++j) {
|
||||
b = v[j];
|
||||
if (a[1] <= y) {
|
||||
if (b[1] > y && isLeft(a, b, p) > 0) ++wn;
|
||||
if (b[1] > y && d3_isCCWTurn(a, b, p)) ++wn;
|
||||
} else {
|
||||
if (b[1] <= y && isLeft(a, b, p) < 0) --wn;
|
||||
if (b[1] <= y && !d3_isCCWTurn(a, b, p)) --wn;
|
||||
}
|
||||
a = b;
|
||||
}
|
||||
}
|
||||
return wn !== 0;
|
||||
}
|
||||
function isLeft(a, b, c) {
|
||||
return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
|
||||
}
|
||||
function interpolate(from, to, direction, listener) {
|
||||
var a = 0, a1 = 0;
|
||||
if (from == null || (a = corner(from, direction)) !== (a1 = corner(to, direction)) || comparePoints(from, to) < 0 ^ direction > 0) {
|
||||
|
@ -4197,65 +4200,17 @@ d3 = function() {
|
|||
if (arguments.length) return hull(vertices);
|
||||
function hull(data) {
|
||||
if (data.length < 3) return [];
|
||||
var fx = d3_functor(x), fy = d3_functor(y), n = data.length, vertices, plen = n - 1, points = [], stack = [], d, i, j, h = 0, x1, y1, x2, y2, u, v, a, sp;
|
||||
if (fx === d3_geom_pointX && y === d3_geom_pointY) vertices = data; else for (i = 0,
|
||||
vertices = []; i < n; ++i) {
|
||||
vertices.push([ +fx.call(this, d = data[i], i), +fy.call(this, d, i) ]);
|
||||
var fx = d3_functor(x), fy = d3_functor(y), i, n = data.length, points = [], flippedPoints = [];
|
||||
for (i = 0; i < n; i++) {
|
||||
points.push([ +fx.call(this, data[i], i), +fy.call(this, data[i], i), i ]);
|
||||
}
|
||||
for (i = 1; i < n; ++i) {
|
||||
if (vertices[i][1] < vertices[h][1] || vertices[i][1] == vertices[h][1] && vertices[i][0] < vertices[h][0]) h = i;
|
||||
}
|
||||
for (i = 0; i < n; ++i) {
|
||||
if (i === h) continue;
|
||||
y1 = vertices[i][1] - vertices[h][1];
|
||||
x1 = vertices[i][0] - vertices[h][0];
|
||||
points.push({
|
||||
angle: Math.atan2(y1, x1),
|
||||
index: i
|
||||
});
|
||||
}
|
||||
points.sort(function(a, b) {
|
||||
return a.angle - b.angle;
|
||||
});
|
||||
a = points[0].angle;
|
||||
v = points[0].index;
|
||||
u = 0;
|
||||
for (i = 1; i < plen; ++i) {
|
||||
j = points[i].index;
|
||||
if (a == points[i].angle) {
|
||||
x1 = vertices[v][0] - vertices[h][0];
|
||||
y1 = vertices[v][1] - vertices[h][1];
|
||||
x2 = vertices[j][0] - vertices[h][0];
|
||||
y2 = vertices[j][1] - vertices[h][1];
|
||||
if (x1 * x1 + y1 * y1 >= x2 * x2 + y2 * y2) {
|
||||
points[i].index = -1;
|
||||
continue;
|
||||
} else {
|
||||
points[u].index = -1;
|
||||
}
|
||||
}
|
||||
a = points[i].angle;
|
||||
u = i;
|
||||
v = j;
|
||||
}
|
||||
stack.push(h);
|
||||
for (i = 0, j = 0; i < 2; ++j) {
|
||||
if (points[j].index > -1) {
|
||||
stack.push(points[j].index);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
sp = stack.length;
|
||||
for (;j < plen; ++j) {
|
||||
if (points[j].index < 0) continue;
|
||||
while (!d3_geom_hullCCW(stack[sp - 2], stack[sp - 1], points[j].index, vertices)) {
|
||||
--sp;
|
||||
}
|
||||
stack[sp++] = points[j].index;
|
||||
}
|
||||
var poly = [];
|
||||
for (i = sp - 1; i >= 0; --i) poly.push(data[stack[i]]);
|
||||
return poly;
|
||||
points.sort(d3_geom_hullOrder);
|
||||
for (i = 0; i < n; i++) flippedPoints.push([ points[i][0], -points[i][1] ]);
|
||||
var upper = d3_geom_hullUpper(points), lower = d3_geom_hullUpper(flippedPoints);
|
||||
var skipLeft = lower[0] === upper[0], skipRight = lower[lower.length - 1] === upper[upper.length - 1], polygon = [];
|
||||
for (i = upper.length - 1; i >= 0; --i) polygon.push(data[points[upper[i]][2]]);
|
||||
for (i = +skipLeft; i < lower.length - skipRight; ++i) polygon.push(data[points[lower[i]][2]]);
|
||||
return polygon;
|
||||
}
|
||||
hull.x = function(_) {
|
||||
return arguments.length ? (x = _, hull) : x;
|
||||
|
@ -4265,18 +4220,18 @@ d3 = function() {
|
|||
};
|
||||
return hull;
|
||||
};
|
||||
function d3_geom_hullCCW(i1, i2, i3, v) {
|
||||
var t, a, b, c, d, e, f;
|
||||
t = v[i1];
|
||||
a = t[0];
|
||||
b = t[1];
|
||||
t = v[i2];
|
||||
c = t[0];
|
||||
d = t[1];
|
||||
t = v[i3];
|
||||
e = t[0];
|
||||
f = t[1];
|
||||
return (f - b) * (c - a) - (d - b) * (e - a) > 0;
|
||||
function d3_geom_hullUpper(points) {
|
||||
var n = points.length, hull = [ 0, 1 ], hs = 2;
|
||||
for (var i = 2; i < n; i++) {
|
||||
while (hs > 1 && !d3_isCCWTurn(points[hull[hs - 2]], points[hull[hs - 1]], points[i])) {
|
||||
hs--;
|
||||
}
|
||||
hull[hs++] = i;
|
||||
}
|
||||
return hull.slice(0, hs);
|
||||
}
|
||||
function d3_geom_hullOrder(a, b) {
|
||||
return a[0] - b[0] || a[1] - b[1];
|
||||
}
|
||||
d3.geom.polygon = function(coordinates) {
|
||||
d3_subclass(coordinates, d3_geom_polygonPrototype);
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -79,9 +79,9 @@ function d3_geo_clipExtent(x0, y0, x1, y1) {
|
|||
for (var j = 1, v = polygon[i], m = v.length, a = v[0], b; j < m; ++j) {
|
||||
b = v[j];
|
||||
if (a[1] <= y) {
|
||||
if (b[1] > y && isLeft(a, b, p) > 0) ++wn;
|
||||
if (b[1] > y && d3_isCCWTurn(a, b, p)) ++wn;
|
||||
} else {
|
||||
if (b[1] <= y && isLeft(a, b, p) < 0) --wn;
|
||||
if (b[1] <= y && !d3_isCCWTurn(a, b, p)) --wn;
|
||||
}
|
||||
a = b;
|
||||
}
|
||||
|
@ -89,10 +89,6 @@ function d3_geo_clipExtent(x0, y0, x1, y1) {
|
|||
return wn !== 0;
|
||||
}
|
||||
|
||||
function isLeft(a, b, c) {
|
||||
return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
|
||||
}
|
||||
|
||||
function interpolate(from, to, direction, listener) {
|
||||
var a = 0, a1 = 0;
|
||||
if (from == null ||
|
||||
|
|
129
src/geom/hull.js
129
src/geom/hull.js
|
@ -1,15 +1,18 @@
|
|||
import "../core/functor";
|
||||
import "../math/trigonometry";
|
||||
import "geom";
|
||||
import "point";
|
||||
|
||||
/**
|
||||
* Computes the 2D convex hull of a set of points using Graham's scanning
|
||||
* algorithm. The algorithm has been implemented as described in Cormen,
|
||||
* Leiserson, and Rivest's Introduction to Algorithms. The running time of
|
||||
* this algorithm is O(n log n), where n is the number of input points.
|
||||
* Computes the 2D convex hull of a set of points using the monotone chain
|
||||
* algorithm:
|
||||
* http://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain)
|
||||
*
|
||||
* @param vertices [[x1, y1], [x2, y2], …]
|
||||
* @returns polygon [[x1, y1], [x2, y2], …]
|
||||
* The runtime of this algorithm is O(n log n), where n is the number of input
|
||||
* points. However in practice it outperforms other O(n log n) hulls.
|
||||
*
|
||||
* @param vertices [[x1, y1], [x2, y2], ...]
|
||||
* @returns polygon [[x1, y1], [x2, y2], ...]
|
||||
*/
|
||||
d3.geom.hull = function(vertices) {
|
||||
var x = d3_geom_pointX,
|
||||
|
@ -18,86 +21,40 @@ d3.geom.hull = function(vertices) {
|
|||
if (arguments.length) return hull(vertices);
|
||||
|
||||
function hull(data) {
|
||||
// Hull of < 3 points is not well-defined
|
||||
if (data.length < 3) return [];
|
||||
|
||||
var fx = d3_functor(x),
|
||||
fy = d3_functor(y),
|
||||
i,
|
||||
n = data.length,
|
||||
vertices, // TODO use parallel arrays
|
||||
plen = n - 1,
|
||||
points = [],
|
||||
stack = [],
|
||||
d,
|
||||
i, j, h = 0, x1, y1, x2, y2, u, v, a, sp;
|
||||
points = [], // of the form [[x0, y0, 0], ..., [xn, yn, n]]
|
||||
flippedPoints = [];
|
||||
|
||||
if (fx === d3_geom_pointX && y === d3_geom_pointY) vertices = data;
|
||||
else for (i = 0, vertices = []; i < n; ++i) {
|
||||
vertices.push([+fx.call(this, d = data[i], i), +fy.call(this, d, i)]);
|
||||
for (i = 0 ; i < n; i++) {
|
||||
points.push([+fx.call(this, data[i], i), +fy.call(this, data[i], i), i]);
|
||||
}
|
||||
|
||||
// find the starting ref point: leftmost point with the minimum y coord
|
||||
for (i = 1; i < n; ++i) {
|
||||
if (vertices[i][1] < vertices[h][1]
|
||||
|| vertices[i][1] == vertices[h][1]
|
||||
&& vertices[i][0] < vertices[h][0]) h = i;
|
||||
}
|
||||
// sort ascending by x-coord first, y-coord second
|
||||
points.sort(d3_geom_hullOrder);
|
||||
|
||||
// calculate polar angles from ref point and sort
|
||||
for (i = 0; i < n; ++i) {
|
||||
if (i === h) continue;
|
||||
y1 = vertices[i][1] - vertices[h][1];
|
||||
x1 = vertices[i][0] - vertices[h][0];
|
||||
points.push({angle: Math.atan2(y1, x1), index: i});
|
||||
}
|
||||
points.sort(function(a, b) { return a.angle - b.angle; });
|
||||
// we flip bottommost points across y axis so we can use the upper hull routine on both
|
||||
for (i = 0; i < n; i++) flippedPoints.push([points[i][0], -points[i][1]]);
|
||||
|
||||
// toss out duplicate angles
|
||||
a = points[0].angle;
|
||||
v = points[0].index;
|
||||
u = 0;
|
||||
for (i = 1; i < plen; ++i) {
|
||||
j = points[i].index;
|
||||
if (a == points[i].angle) {
|
||||
// keep angle for point most distant from the reference
|
||||
x1 = vertices[v][0] - vertices[h][0];
|
||||
y1 = vertices[v][1] - vertices[h][1];
|
||||
x2 = vertices[j][0] - vertices[h][0];
|
||||
y2 = vertices[j][1] - vertices[h][1];
|
||||
if (x1 * x1 + y1 * y1 >= x2 * x2 + y2 * y2) {
|
||||
points[i].index = -1;
|
||||
continue;
|
||||
} else {
|
||||
points[u].index = -1;
|
||||
}
|
||||
}
|
||||
a = points[i].angle;
|
||||
u = i;
|
||||
v = j;
|
||||
}
|
||||
var upper = d3_geom_hullUpper(points),
|
||||
lower = d3_geom_hullUpper(flippedPoints);
|
||||
|
||||
// initialize the stack
|
||||
stack.push(h);
|
||||
for (i = 0, j = 0; i < 2; ++j) {
|
||||
if (points[j].index > -1) {
|
||||
stack.push(points[j].index);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
sp = stack.length;
|
||||
// construct the polygon, removing possible duplicate endpoints
|
||||
var skipLeft = lower[0] === upper[0],
|
||||
skipRight = lower[lower.length - 1] === upper[upper.length - 1],
|
||||
polygon = [];
|
||||
|
||||
// do graham's scan
|
||||
for (; j < plen; ++j) {
|
||||
if (points[j].index < 0) continue; // skip tossed out points
|
||||
while (!d3_geom_hullCCW(stack[sp - 2], stack[sp - 1], points[j].index, vertices)) {
|
||||
--sp;
|
||||
}
|
||||
stack[sp++] = points[j].index;
|
||||
}
|
||||
for (i = upper.length - 1; i >= 0; --i)
|
||||
polygon.push(data[points[upper[i]][2]]); // add upper hull in r->l order
|
||||
for (i = +skipLeft; i < lower.length - skipRight; ++i)
|
||||
polygon.push(data[points[lower[i]][2]]); // add lower hull in l->r order
|
||||
|
||||
// construct the hull
|
||||
var poly = [];
|
||||
for (i = sp - 1; i >= 0; --i) poly.push(data[stack[i]]);
|
||||
return poly;
|
||||
return polygon;
|
||||
}
|
||||
|
||||
hull.x = function(_) {
|
||||
|
@ -111,11 +68,23 @@ d3.geom.hull = function(vertices) {
|
|||
return hull;
|
||||
};
|
||||
|
||||
// are three points in counter-clockwise order?
|
||||
function d3_geom_hullCCW(i1, i2, i3, v) {
|
||||
var t, a, b, c, d, e, f;
|
||||
t = v[i1]; a = t[0]; b = t[1];
|
||||
t = v[i2]; c = t[0]; d = t[1];
|
||||
t = v[i3]; e = t[0]; f = t[1];
|
||||
return (f - b) * (c - a) - (d - b) * (e - a) > 0;
|
||||
// finds the 'upper convex hull' (see wiki link above)
|
||||
// assumes points arg has >=3 elements, is sorted by x, unique in y
|
||||
// returns array of indices into points in left to right order
|
||||
function d3_geom_hullUpper(points) {
|
||||
var n = points.length,
|
||||
hull = [0, 1],
|
||||
hs = 2; // hull size
|
||||
|
||||
for (var i = 2; i < n; i++) {
|
||||
while (hs > 1 && !d3_isCCWTurn(points[hull[hs-2]], points[hull[hs-1]], points[i])) {
|
||||
hs --;
|
||||
}
|
||||
hull[hs++] = i;
|
||||
}
|
||||
// we slice to make sure that the points we 'popped' from hull don't stay behind
|
||||
return hull.slice(0, hs);
|
||||
}
|
||||
|
||||
// comparator for ascending sort by x-coord first, y-coord second
|
||||
function d3_geom_hullOrder(a, b) { return a[0] - b[0] || a[1] - b[1]; }
|
||||
|
|
|
@ -10,6 +10,22 @@ function d3_sgn(x) {
|
|||
return x > 0 ? 1 : x < 0 ? -1 : 0;
|
||||
}
|
||||
|
||||
// returns true iff the [x,y] points a, b, c form a counter-clockwise turn in
|
||||
// the traditional Cartesian coordinate system (i.e. x value grows from left
|
||||
// to right, y value grows from bottom to top)
|
||||
function d3_isCCWTurn(a, b, c) {
|
||||
return d3_cross2d(a, b, c) > 0;
|
||||
}
|
||||
|
||||
// 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross
|
||||
// product, in traditional Cartesian coordinate system (x value grows from
|
||||
// left to right, y value grows from bottom to top). Returns a positive value
|
||||
// if OAB makes a counter-clockwise turn, negative for clockwise turn, and
|
||||
// zero if the points are collinear.
|
||||
function d3_cross2d(o, a, b) {
|
||||
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
|
||||
}
|
||||
|
||||
function d3_acos(x) {
|
||||
return x > 1 ? 0 : x < -1 ? π : Math.acos(x);
|
||||
}
|
||||
|
|
|
@ -28,13 +28,37 @@ suite.addBatch({
|
|||
assert.deepEqual(h([[200, 200], [760, 300]]), []);
|
||||
},
|
||||
"for three points": function(h) {
|
||||
assert.deepEqual(h([[200, 200], [760, 300], [500, 500]]), [[500, 500], [760, 300], [200, 200]]);
|
||||
assert.deepEqual(h([[200, 200], [760, 300], [500, 500]]), [[760, 300], [200, 200], [500, 500]]);
|
||||
},
|
||||
"for four points": function(h) {
|
||||
assert.deepEqual(h([[200, 200], [760, 300], [500, 500], [400, 400]]), [[500, 500], [760, 300], [200, 200]]);
|
||||
assert.deepEqual(h([[200, 200], [760, 300], [500, 500], [400, 400]]), [[760, 300], [200, 200], [500, 500]]);
|
||||
},
|
||||
"returns a counter-clockwise polygon": function(h) {
|
||||
assert.greater(_.geom.polygon(h([[200, 200], [760, 300], [500, 500], [400, 400]])).area(), 0);
|
||||
},
|
||||
"handles points with duplicate ordinates": function(h) {
|
||||
assert.deepEqual(h([[-10, -10], [10, 10], [10, -10], [-10, 10]]), [[10, 10], [10, -10], [-10, -10], [-10, 10]]);
|
||||
},
|
||||
"handles overlapping upper and lower hulls": function(h) {
|
||||
assert.deepEqual(h([[0, -10], [0, 10], [0, 0], [10, 0], [-10, 0]]), [[10, 0], [0, -10], [-10, 0], [0, 10]]);
|
||||
},
|
||||
|
||||
// Cases below taken from http://uva.onlinejudge.org/external/6/681.html
|
||||
|
||||
"for a set of 6 points with non-trivial hull": function(h) {
|
||||
var poly = [[60,20], [250,140], [180,170], [79,140], [50,60], [60,20]];
|
||||
var expectedHull = [[250,140], [60,20], [50,60], [79,140], [180,170]];
|
||||
assert.deepEqual(h(poly), expectedHull);
|
||||
},
|
||||
"for a set of 12 points with non-trivial hull": function(h) {
|
||||
var poly = [[50,60], [60,20], [70,45], [100,70], [125,90], [200,113], [250,140], [180,170], [105,140], [79,140], [60,85], [50,60]];
|
||||
var expectedHull = [[250,140], [60,20], [50,60], [79,140], [180,170]];
|
||||
assert.deepEqual(h(poly), expectedHull);
|
||||
},
|
||||
"for a set of 15 points with non-trivial hull": function(h) {
|
||||
var poly = [[30,30], [50,60], [60,20], [70,45], [86,39], [112,60], [200,113], [250,50], [300,200], [130,240], [76,150], [47,76], [36,40], [33,35], [30,30]];
|
||||
var expectedHull = [[300,200], [250,50], [60,20], [30,30], [47,76], [76,150], [130,240]];
|
||||
assert.deepEqual(h(poly), expectedHull);
|
||||
}
|
||||
},
|
||||
"the hull layout with custom accessors": {
|
||||
|
@ -42,7 +66,7 @@ suite.addBatch({
|
|||
return hull().x(function(d) { return d.x; }).y(function(d) { return d.y; });
|
||||
},
|
||||
"of four points": function(h) {
|
||||
assert.deepEqual(h([{x: 200, y: 200}, {x: 760, y: 300}, {x: 500, y: 500}, {x: 400, y: 400}]), [{x: 500, y: 500}, {x: 760, y: 300}, {x: 200, y: 200}]);
|
||||
assert.deepEqual(h([{x: 200, y: 200}, {x: 760, y: 300}, {x: 500, y: 500}, {x: 400, y: 400}]), [{x: 760, y: 300}, {x: 200, y: 200}, {x: 500, y: 500}]);
|
||||
}
|
||||
},
|
||||
"the default hull layout applied directly": {
|
||||
|
@ -56,10 +80,10 @@ suite.addBatch({
|
|||
return h([[200, 200], [760, 300]]);
|
||||
},
|
||||
"for three points": function(h) {
|
||||
assert.deepEqual(h([[200, 200], [760, 300], [500, 500]]), [[500, 500], [760, 300], [200, 200]]);
|
||||
assert.deepEqual(h([[200, 200], [760, 300], [500, 500]]), [[760, 300], [200, 200], [500, 500]]);
|
||||
},
|
||||
"for four points": function(h) {
|
||||
assert.deepEqual(h([[200, 200], [760, 300], [500, 500], [400, 400]]), [[500, 500], [760, 300], [200, 200]]);
|
||||
assert.deepEqual(h([[200, 200], [760, 300], [500, 500], [400, 400]]), [[760, 300], [200, 200], [500, 500]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче