Added convex hull at d3.geom.hull

This commit is contained in:
Jeffrey Heer 2010-11-22 16:21:46 -08:00
Родитель 9761e25a4a
Коммит bbe029fa7f
6 изменённых файлов: 311 добавлений и 11 удалений

Просмотреть файл

@ -79,6 +79,7 @@ d3.time.js: \
d3.geom.js: \
src/geom/geom.js \
src/geom/hull.js \
src/geom/polygon.js \
src/geom/voronoi.js \
src/geom/delaunay.js

Просмотреть файл

@ -1,4 +1,109 @@
d3.geom = {};
/**
* 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.
*
* @param vertices [[x1, y1], [x2, y2], ]
* @returns polygon [[x1, y1], [x2, y2], ],
*/
d3.geom.hull = function(vertices) {
// helper method to detect a non-left turn about 3 points
var isNonLeft = function(i0, i1, i2, i3, v) {
var x, y, l1, l2, l4, l5, l6, a1, a2;
y = v[i2][1]-v[i1][1]; x = v[i2][0]-v[i1][0]; l1 = x*x + y*y;
y = v[i3][1]-v[i2][1]; x = v[i3][0]-v[i2][0]; l2 = x*x + y*y;
y = v[i3][1]-v[i0][1]; x = v[i3][0]-v[i0][0]; l4 = x*x + y*y;
y = v[i1][1]-v[i0][1]; x = v[i1][0]-v[i0][0]; l5 = x*x + y*y;
y = v[i2][1]-v[i0][1]; x = v[i2][0]-v[i0][0]; l6 = x*x + y*y;
a1 = Math.acos((l2+l6-l4) / (2*Math.sqrt(l2*l6)));
a2 = Math.acos((l6+l1-l5) / (2*Math.sqrt(l6*l1)));
return ((Math.PI-a1) - a2) <= 0.0;
}
if (vertices.length < 3) return [];
var len = vertices.length,
plen = len - 1,
points = [],
stack = [],
i, j, h = 0, x1, y1, x2, y2, u, v, a, sp;
// find the starting ref point: leftmost point with the minimum y coord
for (i=1; i<len; ++i) {
if (vertices[i][1] < vertices[h][1]) {
h = i;
} else if (vertices[i][1] == vertices[h][1]) {
h = (vertices[i][0] < vertices[h][0] ? i : h);
}
}
// calculate polar angles from ref point and sort
for (i=0; i<len; ++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; });
// 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;
} else {
points[u].index = -1;
a = points[i].angle;
u = i;
v = j;
}
} else {
a = points[i].angle;
u = i;
v = j;
}
}
// 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;
// do graham's scan
for (; j<plen; ++j) {
if (points[j].index == -1) continue; // skip tossed out points
while (isNonLeft(h, stack[sp-2], stack[sp-1], points[j].index, vertices)) {
--sp;
}
stack[sp++] = points[j].index;
}
// construct the hull
var poly = [];
for (i=0; i<sp; ++i) {
poly.push(vertices[stack[i]]);
}
return poly;
}
// Note: requires coordinates to be counterclockwise and convex!
d3.geom.polygon = function(coordinates) {

26
d3.geom.min.js поставляемый
Просмотреть файл

@ -1,11 +1,15 @@
(function(){var r=null;d3.geom={};d3.geom.polygon=function(j){j.area=function(){for(var k=0,e=j.length,a=j[e-1][0]*j[0][1],g=j[e-1][1]*j[0][0];++k<e;){a+=j[k-1][0]*j[k][1];g+=j[k-1][1]*j[k][0]}return(g-a)*0.5};j.clip=function(k){for(var e,a=-1,g=j.length,i,l,n=j[g-1],h,o,p;++a<g;){e=k.slice();k.length=0;h=j[a];o=e[(l=e.length)-1];for(i=-1;++i<l;){p=e[i];if(A(p,n,h)){A(o,n,h)||k.push(B(o,p,n,h));k.push(p)}else A(o,n,h)&&k.push(B(o,p,n,h));o=p}n=h}return k};return j};
function A(j,k,e){return(e[0]-k[0])*(j[1]-k[1])<(e[1]-k[1])*(j[0]-k[0])}function B(j,k,e,a){var g=j[0],i=e[0];j=j[1];var l=e[1];e=k[0]-g;var n=a[0]-i;k=k[1]-j;a=a[1]-l;i=(n*(j-l)-a*(g-i))/(a*e-n*k);return[g+i*e,j+i*k]}
d3.geom.voronoi=function(j){var k=j.map(function(){return[]});C(j,function(e){var a,g,i,l;if(e.d==1&&e.b>=0){a=e.o.r;g=e.o.l}else{a=e.o.l;g=e.o.r}if(e.d==1){i=a?a.y:-1E6;a=e.a-e.b*i;l=g?g.y:1E6;g=e.a-e.b*l}else{a=a?a.x:-1E6;i=e.a-e.d*a;g=g?g.x:1E6;l=e.a-e.d*g}a=[a,i];g=[g,l];k[e.region.l.index].push(a,g);k[e.region.r.index].push(a,g)});return k.map(function(e,a){var g=j[a][0],i=j[a][1];e.forEach(function(l){l.m=Math.atan2(l[0]-g,l[1]-i)});return e.sort(function(l,n){return l.m-n.m}).filter(function(l,
n){return!n||l.m-e[n-1].m>1.0E-10})})};var D={l:"r",r:"l"};
function C(j,k){var e={c:j.map(function(b,d){return{index:d,x:b[0],y:b[1]}}).sort(function(b,d){return b.y<d.y?-1:b.y>d.y?1:b.x<d.x?-1:b.x>d.x?1:0}),A:r},a={c:[],p:r,q:r,G:function(){a.p=a.t(r,"l");a.q=a.t(r,"l");a.p.r=a.q;a.q.l=a.p;a.c.unshift(a.p,a.q)},t:function(b,d){return{i:b,j:d,z:r,l:r,r:r}},k:function(b,d){d.l=b;d.r=b.r;b.r.l=d;b.r=d},H:function(b){var d=a.p;do d=d.r;while(d!=a.q&&g.J(d,b));return d=d.l},n:function(b){b.l.r=b.r;b.r.l=b.l;b.i=r},right:function(b){return b.r},left:function(b){return b.l},
I:function(b){return b.i==r?e.A:b.i.region[b.j]},D:function(b){return b.i==r?e.A:b.i.region[D[b.j]]}},g={C:function(b,d){var c={region:{l:b,r:d},o:{l:r,r:r}},f=d.x-b.x,m=d.y-b.y,t=f>0?f:-f,s=m>0?m:-m;c.a=b.x*f+b.y*m+(f*f+m*m)*0.5;if(t>s){c.d=1;c.b=m/f;c.a/=f}else{c.b=1;c.d=f/m;c.a/=m}return c},w:function(b,d){var c=b.i,f=d.i;if(!c||!f||c.region.r==f.region.r)return r;var m=c.d*f.b-c.b*f.d;if(Math.abs(m)<1.0E-10)return r;var t=(c.a*f.b-f.a*c.b)/m;m=(f.a*c.d-c.a*f.d)/m;var s=c.region.r,x=f.region.r;
if(s.y<x.y||s.y==x.y&&s.x<x.x){s=b;c=c}else{s=d;c=f}if((c=t>=c.region.r.x)&&s.j=="l"||!c&&s.j=="r")return r;return{x:t,y:m}},J:function(b,d){var c=b.i,f=c.region.r,m=d.x>f.x;if(m&&b.j=="l")return 1;if(!m&&b.j=="r")return 0;if(c.d==1){var t=d.y-f.y,s=d.x-f.x,x=0,v=0;if(!m&&c.b<0||m&&c.b>=0)v=x=t>=c.b*s;else{v=d.x+d.y*c.b>c.a;if(c.b<0)v=!v;v||(x=1)}if(!x){f=f.x-c.region.l.x;v=c.b*(s*s-t*t)<f*t*(1+2*s/f+c.b*c.b);if(c.b<0)v=!v}}else{s=c.a-c.d*d.x;c=d.y-s;t=d.x-f.x;f=s-f.y;v=c*c>t*t+f*f}return b.j=="l"?
v:!v},B:function(b,d,c){b.o[d]=c;b.o[D[d]]&&k(b)},v:function(b,d){var c=b.x-d.x,f=b.y-d.y;return Math.sqrt(c*c+f*f)}},i={c:[],k:function(b,d,c){b.z=d;b.u=d.y+c;c=0;for(var f=i.c,m=f.length;c<m;c++){var t=f[c];if(!(b.u>t.u||b.u==t.u&&d.x>t.z.x))break}f.splice(c,0,b)},n:function(b){for(var d=0,c=i.c,f=c.length;d<f&&c[d]!=b;++d);c.splice(d,1)},empty:function(){return i.c.length==0},K:function(b){for(var d=0,c=i.c,f=c.length;d<f;++d)if(c[d]==b)return c[d+1];return r},min:function(){var b=i.c[0];return{x:b.z.x,
y:b.u}},F:function(){return i.c.shift()}};a.G();e.A=e.c.shift();for(var l=e.c.shift(),n,h,o,p,z,q,w,u,y;;){i.empty()||(n=i.min());if(l&&(i.empty()||l.y<n.y||l.y==n.y&&l.x<n.x)){h=a.H(l);o=a.right(h);w=a.D(h);y=g.C(w,l);q=a.t(y,"l");a.k(h,q);if(u=g.w(h,q)){i.n(h);i.k(h,u,g.v(u,l))}h=q;q=a.t(y,"r");a.k(h,q);(u=g.w(q,o))&&i.k(q,u,g.v(u,l));l=e.c.shift()}else if(i.empty())break;else{h=i.F();p=a.left(h);o=a.right(h);z=a.right(o);w=a.I(h);q=a.D(o);u=h.z;g.B(h.i,h.j,u);g.B(o.i,o.j,u);a.n(h);i.n(o);a.n(o);
h="l";if(w.y>q.y){h=w;w=q;q=h;h="r"}y=g.C(w,q);q=a.t(y,h);a.k(p,q);g.B(y,D[h],u);if(u=g.w(p,q)){i.n(p);i.k(p,u,g.v(u,w))}(u=g.w(q,z))&&i.k(q,u,g.v(u,w))}}for(h=a.right(a.p);h!=a.q;h=a.right(h))k(h.i)}
d3.geom.delaunay=function(j){var k=j.map(function(){return[]}),e=[];C(j,function(a){k[a.region.l.index].push(j[a.region.r.index])});k.forEach(function(a,g){var i=j[g],l=i[0],n=i[1];a.forEach(function(p){p.m=Math.atan2(p[0]-l,p[1]-n)});a.sort(function(p,z){return p.m-z.m});for(var h=0,o=a.length-1;h<o;h++)e.push([i,a[h],a[h+1]])});return e};})()
(function(){var v=null;d3.geom={};
d3.geom.hull=function(h){function n(u,b,f,c,e){var l,o,s,x;o=e[f][1]-e[b][1];l=e[f][0]-e[b][0];s=l*l+o*o;o=e[c][1]-e[f][1];l=e[c][0]-e[f][0];x=l*l+o*o;o=e[c][1]-e[u][1];l=e[c][0]-e[u][0];c=l*l+o*o;o=e[b][1]-e[u][1];l=e[b][0]-e[u][0];b=l*l+o*o;o=e[f][1]-e[u][1];l=e[f][0]-e[u][0];u=l*l+o*o;return Math.PI-Math.acos((x+u-c)/(2*Math.sqrt(x*u)))-Math.acos((u+s-b)/(2*Math.sqrt(u*s)))<=0}if(h.length<3)return[];var d=h.length,a=d-1,i=[],k=[],g,m=0,j,p,r,z,q,w,t;for(g=1;g<d;++g)if(h[g][1]<h[m][1])m=g;else if(h[g][1]==
h[m][1])m=h[g][0]<h[m][0]?g:m;for(g=0;g<d;++g)if(g!=m){p=h[g][1]-h[m][1];j=h[g][0]-h[m][0];i.push({a:Math.atan2(p,j),index:g})}i.sort(function(u,b){return u.a-b.a});t=i[0].a;w=i[0].index;q=0;for(g=1;g<a;++g){d=i[g].index;if(t==i[g].a){j=h[w][0]-h[m][0];p=h[w][1]-h[m][1];r=h[d][0]-h[m][0];z=h[d][1]-h[m][1];if(j*j+p*p>=r*r+z*z)i[g].index=-1;else{i[q].index=-1;t=i[g].a;q=g;w=d}}else{t=i[g].a;q=g;w=d}}k.push(m);for(d=g=0;g<2;++d)if(i[d].index!=-1){k.push(i[d].index);g++}for(j=k.length;d<a;++d)if(i[d].index!=
-1){for(;n(m,k[j-2],k[j-1],i[d].index,h);)--j;k[j++]=i[d].index}a=[];for(g=0;g<j;++g)a.push(h[k[g]]);return a};
d3.geom.polygon=function(h){h.area=function(){for(var n=0,d=h.length,a=h[d-1][0]*h[0][1],i=h[d-1][1]*h[0][0];++n<d;){a+=h[n-1][0]*h[n][1];i+=h[n-1][1]*h[n][0]}return(i-a)*0.5};h.clip=function(n){for(var d,a=-1,i=h.length,k,g,m=h[i-1],j,p,r;++a<i;){d=n.slice();n.length=0;j=h[a];p=d[(g=d.length)-1];for(k=-1;++k<g;){r=d[k];if(A(r,m,j)){A(p,m,j)||n.push(B(p,r,m,j));n.push(r)}else A(p,m,j)&&n.push(B(p,r,m,j));p=r}m=j}return n};return h};
function A(h,n,d){return(d[0]-n[0])*(h[1]-n[1])<(d[1]-n[1])*(h[0]-n[0])}function B(h,n,d,a){var i=h[0],k=d[0];h=h[1];var g=d[1];d=n[0]-i;var m=a[0]-k;n=n[1]-h;a=a[1]-g;k=(m*(h-g)-a*(i-k))/(a*d-m*n);return[i+k*d,h+k*n]}
d3.geom.voronoi=function(h){var n=h.map(function(){return[]});C(h,function(d){var a,i,k,g;if(d.i==1&&d.b>=0){a=d.o.r;i=d.o.l}else{a=d.o.l;i=d.o.r}if(d.i==1){k=a?a.y:-1E6;a=d.c-d.b*k;g=i?i.y:1E6;i=d.c-d.b*g}else{a=a?a.x:-1E6;k=d.c-d.i*a;i=i?i.x:1E6;g=d.c-d.i*i}a=[a,k];i=[i,g];n[d.region.l.index].push(a,i);n[d.region.r.index].push(a,i)});return n.map(function(d,a){var i=h[a][0],k=h[a][1];d.forEach(function(g){g.a=Math.atan2(g[0]-i,g[1]-k)});return d.sort(function(g,m){return g.a-m.a}).filter(function(g,
m){return!m||g.a-d[m-1].a>1.0E-10})})};var D={l:"r",r:"l"};
function C(h,n){var d={d:h.map(function(b,f){return{index:f,x:b[0],y:b[1]}}).sort(function(b,f){return b.y<f.y?-1:b.y>f.y?1:b.x<f.x?-1:b.x>f.x?1:0}),A:v},a={d:[],p:v,q:v,G:function(){a.p=a.t(v,"l");a.q=a.t(v,"l");a.p.r=a.q;a.q.l=a.p;a.d.unshift(a.p,a.q)},t:function(b,f){return{j:b,k:f,z:v,l:v,r:v}},m:function(b,f){f.l=b;f.r=b.r;b.r.l=f;b.r=f},H:function(b){var f=a.p;do f=f.r;while(f!=a.q&&i.J(f,b));return f=f.l},n:function(b){b.l.r=b.r;b.r.l=b.l;b.j=v},right:function(b){return b.r},left:function(b){return b.l},
I:function(b){return b.j==v?d.A:b.j.region[b.k]},D:function(b){return b.j==v?d.A:b.j.region[D[b.k]]}},i={C:function(b,f){var c={region:{l:b,r:f},o:{l:v,r:v}},e=f.x-b.x,l=f.y-b.y,o=e>0?e:-e,s=l>0?l:-l;c.c=b.x*e+b.y*l+(e*e+l*l)*0.5;if(o>s){c.i=1;c.b=l/e;c.c/=e}else{c.b=1;c.i=e/l;c.c/=l}return c},w:function(b,f){var c=b.j,e=f.j;if(!c||!e||c.region.r==e.region.r)return v;var l=c.i*e.b-c.b*e.i;if(Math.abs(l)<1.0E-10)return v;var o=(c.c*e.b-e.c*c.b)/l;l=(e.c*c.i-c.c*e.i)/l;var s=c.region.r,x=e.region.r;
if(s.y<x.y||s.y==x.y&&s.x<x.x){s=b;c=c}else{s=f;c=e}if((c=o>=c.region.r.x)&&s.k=="l"||!c&&s.k=="r")return v;return{x:o,y:l}},J:function(b,f){var c=b.j,e=c.region.r,l=f.x>e.x;if(l&&b.k=="l")return 1;if(!l&&b.k=="r")return 0;if(c.i==1){var o=f.y-e.y,s=f.x-e.x,x=0,y=0;if(!l&&c.b<0||l&&c.b>=0)y=x=o>=c.b*s;else{y=f.x+f.y*c.b>c.c;if(c.b<0)y=!y;y||(x=1)}if(!x){e=e.x-c.region.l.x;y=c.b*(s*s-o*o)<e*o*(1+2*s/e+c.b*c.b);if(c.b<0)y=!y}}else{s=c.c-c.i*f.x;c=f.y-s;o=f.x-e.x;e=s-e.y;y=c*c>o*o+e*e}return b.k=="l"?
y:!y},B:function(b,f,c){b.o[f]=c;b.o[D[f]]&&n(b)},v:function(b,f){var c=b.x-f.x,e=b.y-f.y;return Math.sqrt(c*c+e*e)}},k={d:[],m:function(b,f,c){b.z=f;b.u=f.y+c;c=0;for(var e=k.d,l=e.length;c<l;c++){var o=e[c];if(!(b.u>o.u||b.u==o.u&&f.x>o.z.x))break}e.splice(c,0,b)},n:function(b){for(var f=0,c=k.d,e=c.length;f<e&&c[f]!=b;++f);c.splice(f,1)},empty:function(){return k.d.length==0},K:function(b){for(var f=0,c=k.d,e=c.length;f<e;++f)if(c[f]==b)return c[f+1];return v},min:function(){var b=k.d[0];return{x:b.z.x,
y:b.u}},F:function(){return k.d.shift()}};a.G();d.A=d.d.shift();for(var g=d.d.shift(),m,j,p,r,z,q,w,t,u;;){k.empty()||(m=k.min());if(g&&(k.empty()||g.y<m.y||g.y==m.y&&g.x<m.x)){j=a.H(g);p=a.right(j);w=a.D(j);u=i.C(w,g);q=a.t(u,"l");a.m(j,q);if(t=i.w(j,q)){k.n(j);k.m(j,t,i.v(t,g))}j=q;q=a.t(u,"r");a.m(j,q);(t=i.w(q,p))&&k.m(q,t,i.v(t,g));g=d.d.shift()}else if(k.empty())break;else{j=k.F();r=a.left(j);p=a.right(j);z=a.right(p);w=a.I(j);q=a.D(p);t=j.z;i.B(j.j,j.k,t);i.B(p.j,p.k,t);a.n(j);k.n(p);a.n(p);
j="l";if(w.y>q.y){j=w;w=q;q=j;j="r"}u=i.C(w,q);q=a.t(u,j);a.m(r,q);i.B(u,D[j],t);if(t=i.w(r,q)){k.n(r);k.m(r,t,i.v(t,w))}(t=i.w(q,z))&&k.m(q,t,i.v(t,w))}}for(j=a.right(a.p);j!=a.q;j=a.right(j))n(j.j)}
d3.geom.delaunay=function(h){var n=h.map(function(){return[]}),d=[];C(h,function(a){n[a.region.l.index].push(h[a.region.r.index])});n.forEach(function(a,i){var k=h[i],g=k[0],m=k[1];a.forEach(function(r){r.a=Math.atan2(r[0]-g,r[1]-m)});a.sort(function(r,z){return r.a-z.a});for(var j=0,p=a.length-1;j<p;j++)d.push([k,a[j],a[j+1]])});return d};})()

84
examples/hull/hull.html Normal file
Просмотреть файл

@ -0,0 +1,84 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>Convex Hull</title>
<script type="text/javascript" src="../../d3.min.js"></script>
<script type="text/javascript" src="../../d3.geom.min.js"></script>
<style type="text/css">
@import url("../../lib/colorbrewer/colorbrewer.css");
svg {
border: solid 1px #666;
}
path {
fill: yellow;
stroke: #000;
stroke-width: .5px;
}
circle {
fill: #ccc;
stroke: #000;
pointer-events: none;
}
</style>
</head>
<body>
<script type="text/javascript">
var w = 960,
h = 500;
var vertices = d3.range(15).map(function(d) {
return [w/4 + Math.random() * w/2,
h/4 + Math.random() * h/2];
});
vertices[0] = [w/2,h/2];
var svg = d3.select("body")
.append("svg:svg")
.attr("width", w)
.attr("height", h)
.attr("class", "PiYG")
.on("mousemove", move)
.on("click", click);
// background rect to catch events
svg.append("svg:rect")
.attr("width", w)
.attr("height", h)
.attr("fill", "white");
update();
function update() {
svg.selectAll("path")
.data([d3.geom.hull(vertices)])
.attr("d", function(d) { return "M" + d.join("L") + "Z"; })
.enter("svg:path")
.attr("class", "q3-9")
.attr("d", function(d) { return "M" + d.join("L") + "Z"; });
svg.selectAll("circle")
.data(vertices.slice(1))
.enter("svg:circle")
.attr("transform", function(d) { return "translate(" + d + ")"; })
.attr("r", 3);
}
function move() {
vertices[0] = d3.svg.mouse(this);
update();
}
function click() {
vertices.push(d3.svg.mouse(this));
update();
}
</script>
</body>
</html>

Просмотреть файл

@ -166,6 +166,7 @@ d3.geo = {
// d3.geom
d3.geom = {
hull: 1,
polygon: {
area: 1,
intersection: 1

105
src/geom/hull.js Normal file
Просмотреть файл

@ -0,0 +1,105 @@
/**
* 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.
*
* @param vertices [[x1, y1], [x2, y2], ]
* @returns polygon [[x1, y1], [x2, y2], ],
*/
d3.geom.hull = function(vertices) {
// helper method to detect a non-left turn about 3 points
var isNonLeft = function(i0, i1, i2, i3, v) {
var x, y, l1, l2, l4, l5, l6, a1, a2;
y = v[i2][1]-v[i1][1]; x = v[i2][0]-v[i1][0]; l1 = x*x + y*y;
y = v[i3][1]-v[i2][1]; x = v[i3][0]-v[i2][0]; l2 = x*x + y*y;
y = v[i3][1]-v[i0][1]; x = v[i3][0]-v[i0][0]; l4 = x*x + y*y;
y = v[i1][1]-v[i0][1]; x = v[i1][0]-v[i0][0]; l5 = x*x + y*y;
y = v[i2][1]-v[i0][1]; x = v[i2][0]-v[i0][0]; l6 = x*x + y*y;
a1 = Math.acos((l2+l6-l4) / (2*Math.sqrt(l2*l6)));
a2 = Math.acos((l6+l1-l5) / (2*Math.sqrt(l6*l1)));
return ((Math.PI-a1) - a2) <= 0.0;
}
if (vertices.length < 3) return [];
var len = vertices.length,
plen = len - 1,
points = [],
stack = [],
i, j, h = 0, x1, y1, x2, y2, u, v, a, sp;
// find the starting ref point: leftmost point with the minimum y coord
for (i=1; i<len; ++i) {
if (vertices[i][1] < vertices[h][1]) {
h = i;
} else if (vertices[i][1] == vertices[h][1]) {
h = (vertices[i][0] < vertices[h][0] ? i : h);
}
}
// calculate polar angles from ref point and sort
for (i=0; i<len; ++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; });
// 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;
} else {
points[u].index = -1;
a = points[i].angle;
u = i;
v = j;
}
} else {
a = points[i].angle;
u = i;
v = j;
}
}
// 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;
// do graham's scan
for (; j<plen; ++j) {
if (points[j].index == -1) continue; // skip tossed out points
while (isNonLeft(h, stack[sp-2], stack[sp-1], points[j].index, vertices)) {
--sp;
}
stack[sp++] = points[j].index;
}
// construct the hull
var poly = [];
for (i=0; i<sp; ++i) {
poly.push(vertices[stack[i]]);
}
return poly;
}