11import "../core/functor" ;
2+ import "../math/trigonometry" ;
23import "geom" ;
34import "point" ;
45
56/**
6- * Computes the 2D convex hull of a set of points using Graham's scanning
7- * algorithm. The algorithm has been implemented as described in Cormen,
8- * Leiserson, and Rivest's Introduction to Algorithms. The running time of
9- * this algorithm is O(n log n), where n is the number of input points.
7+ * Computes the 2D convex hull of a set of points using the monotone chain
8+ * algorithm:
9+ * http://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain)
1010 *
11- * @param vertices [[x1, y1], [x2, y2], …]
12- * @returns polygon [[x1, y1], [x2, y2], …]
11+ * The runtime of this algorithm is O(n log n), where n is the number of input
12+ * points. However in practice it outperforms other O(n log n) hulls.
13+ *
14+ * @param vertices [[x1, y1], [x2, y2], ...]
15+ * @returns polygon [[x1, y1], [x2, y2], ...]
1316 */
1417d3 . geom . hull = function ( vertices ) {
1518 var x = d3_geom_pointX ,
@@ -18,86 +21,40 @@ d3.geom.hull = function(vertices) {
1821 if ( arguments . length ) return hull ( vertices ) ;
1922
2023 function hull ( data ) {
24+ // Hull of < 3 points is not well-defined
2125 if ( data . length < 3 ) return [ ] ;
2226
2327 var fx = d3_functor ( x ) ,
2428 fy = d3_functor ( y ) ,
29+ i ,
2530 n = data . length ,
26- vertices , // TODO use parallel arrays
27- plen = n - 1 ,
28- points = [ ] ,
29- stack = [ ] ,
30- d ,
31- i , j , h = 0 , x1 , y1 , x2 , y2 , u , v , a , sp ;
32-
33- if ( fx === d3_geom_pointX && y === d3_geom_pointY ) vertices = data ;
34- else for ( i = 0 , vertices = [ ] ; i < n ; ++ i ) {
35- vertices . push ( [ + fx . call ( this , d = data [ i ] , i ) , + fy . call ( this , d , i ) ] ) ;
36- }
31+ points = [ ] , // of the form [[x0, y0, 0], ..., [xn, yn, n]]
32+ flippedPoints = [ ] ;
3733
38- // find the starting ref point: leftmost point with the minimum y coord
39- for ( i = 1 ; i < n ; ++ i ) {
40- if ( vertices [ i ] [ 1 ] < vertices [ h ] [ 1 ]
41- || vertices [ i ] [ 1 ] == vertices [ h ] [ 1 ]
42- && vertices [ i ] [ 0 ] < vertices [ h ] [ 0 ] ) h = i ;
34+ for ( i = 0 ; i < n ; i ++ ) {
35+ points . push ( [ + fx . call ( this , data [ i ] , i ) , + fy . call ( this , data [ i ] , i ) , i ] ) ;
4336 }
4437
45- // calculate polar angles from ref point and sort
46- for ( i = 0 ; i < n ; ++ i ) {
47- if ( i === h ) continue ;
48- y1 = vertices [ i ] [ 1 ] - vertices [ h ] [ 1 ] ;
49- x1 = vertices [ i ] [ 0 ] - vertices [ h ] [ 0 ] ;
50- points . push ( { angle : Math . atan2 ( y1 , x1 ) , index : i } ) ;
51- }
52- points . sort ( function ( a , b ) { return a . angle - b . angle ; } ) ;
53-
54- // toss out duplicate angles
55- a = points [ 0 ] . angle ;
56- v = points [ 0 ] . index ;
57- u = 0 ;
58- for ( i = 1 ; i < plen ; ++ i ) {
59- j = points [ i ] . index ;
60- if ( a == points [ i ] . angle ) {
61- // keep angle for point most distant from the reference
62- x1 = vertices [ v ] [ 0 ] - vertices [ h ] [ 0 ] ;
63- y1 = vertices [ v ] [ 1 ] - vertices [ h ] [ 1 ] ;
64- x2 = vertices [ j ] [ 0 ] - vertices [ h ] [ 0 ] ;
65- y2 = vertices [ j ] [ 1 ] - vertices [ h ] [ 1 ] ;
66- if ( x1 * x1 + y1 * y1 >= x2 * x2 + y2 * y2 ) {
67- points [ i ] . index = - 1 ;
68- continue ;
69- } else {
70- points [ u ] . index = - 1 ;
71- }
72- }
73- a = points [ i ] . angle ;
74- u = i ;
75- v = j ;
76- }
38+ // sort ascending by x-coord first, y-coord second
39+ points . sort ( d3_geom_hullOrder ) ;
7740
78- // initialize the stack
79- stack . push ( h ) ;
80- for ( i = 0 , j = 0 ; i < 2 ; ++ j ) {
81- if ( points [ j ] . index > - 1 ) {
82- stack . push ( points [ j ] . index ) ;
83- i ++ ;
84- }
85- }
86- sp = stack . length ;
87-
88- // do graham's scan
89- for ( ; j < plen ; ++ j ) {
90- if ( points [ j ] . index < 0 ) continue ; // skip tossed out points
91- while ( ! d3_geom_hullCCW ( stack [ sp - 2 ] , stack [ sp - 1 ] , points [ j ] . index , vertices ) ) {
92- -- sp ;
93- }
94- stack [ sp ++ ] = points [ j ] . index ;
95- }
41+ // we flip bottommost points across y axis so we can use the upper hull routine on both
42+ for ( i = 0 ; i < n ; i ++ ) flippedPoints . push ( [ points [ i ] [ 0 ] , - points [ i ] [ 1 ] ] ) ;
43+
44+ var upper = d3_geom_hullUpper ( points ) ,
45+ lower = d3_geom_hullUpper ( flippedPoints ) ;
9646
97- // construct the hull
98- var poly = [ ] ;
99- for ( i = sp - 1 ; i >= 0 ; -- i ) poly . push ( data [ stack [ i ] ] ) ;
100- return poly ;
47+ // construct the polygon, removing possible duplicate endpoints
48+ var skipLeft = lower [ 0 ] === upper [ 0 ] ,
49+ skipRight = lower [ lower . length - 1 ] === upper [ upper . length - 1 ] ,
50+ polygon = [ ] ;
51+
52+ for ( i = upper . length - 1 ; i >= 0 ; -- i )
53+ polygon . push ( data [ points [ upper [ i ] ] [ 2 ] ] ) ; // add upper hull in r->l order
54+ for ( i = + skipLeft ; i < lower . length - skipRight ; ++ i )
55+ polygon . push ( data [ points [ lower [ i ] ] [ 2 ] ] ) ; // add lower hull in l->r order
56+
57+ return polygon ;
10158 }
10259
10360 hull . x = function ( _ ) {
@@ -111,11 +68,23 @@ d3.geom.hull = function(vertices) {
11168 return hull ;
11269} ;
11370
114- // are three points in counter-clockwise order?
115- function d3_geom_hullCCW ( i1 , i2 , i3 , v ) {
116- var t , a , b , c , d , e , f ;
117- t = v [ i1 ] ; a = t [ 0 ] ; b = t [ 1 ] ;
118- t = v [ i2 ] ; c = t [ 0 ] ; d = t [ 1 ] ;
119- t = v [ i3 ] ; e = t [ 0 ] ; f = t [ 1 ] ;
120- return ( f - b ) * ( c - a ) - ( d - b ) * ( e - a ) > 0 ;
71+ // finds the 'upper convex hull' (see wiki link above)
72+ // assumes points arg has >=3 elements, is sorted by x, unique in y
73+ // returns array of indices into points in left to right order
74+ function d3_geom_hullUpper ( points ) {
75+ var n = points . length ,
76+ hull = [ 0 , 1 ] ,
77+ hs = 2 ; // hull size
78+
79+ for ( var i = 2 ; i < n ; i ++ ) {
80+ while ( hs > 1 && ! d3_isCCWTurn ( points [ hull [ hs - 2 ] ] , points [ hull [ hs - 1 ] ] , points [ i ] ) ) {
81+ hs -- ;
82+ }
83+ hull [ hs ++ ] = i ;
84+ }
85+ // we slice to make sure that the points we 'popped' from hull don't stay behind
86+ return hull . slice ( 0 , hs ) ;
12187}
88+
89+ // comparator for ascending sort by x-coord first, y-coord second
90+ function d3_geom_hullOrder ( a , b ) { return a [ 0 ] - b [ 0 ] || a [ 1 ] - b [ 1 ] ; }
0 commit comments