-
Notifications
You must be signed in to change notification settings - Fork 3
/
graham-scan.mjs
133 lines (115 loc) · 3.73 KB
/
graham-scan.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
const X = 0;
const Y = 1;
const REMOVED = -1;
export default class GrahamScan {
constructor() {
/** @type {[Number, Number][]} */
this.points = [];
}
clear() {
this.points = [];
}
getPoints() {
return this.points;
}
setPoints(points) {
this.points = points.slice(); // copy
}
addPoint(point) {
this.points.push(point);
}
/**
* Returns the smallest convex hull of a given set of points. Runs in O(n log n).
*
* @return {[Number, Number][]}
*/
getHull() {
const pivot = this.preparePivotPoint();
let indexes = Array.from(this.points, (point, i) => i);
const angles = Array.from(this.points, (point) => this.getAngle(pivot, point));
const distances = Array.from(this.points, (point) => this.euclideanDistanceSquared(pivot, point));
// sort by angle and distance
indexes.sort((i, j) => {
const angleA = angles[i];
const angleB = angles[j];
if (angleA === angleB) {
const distanceA = distances[i];
const distanceB = distances[j];
return distanceA - distanceB;
}
return angleA - angleB;
});
// remove points with repeated angle (but never the pivot, so start from i=1)
for (let i = 1; i < indexes.length - 1; i++) {
if (angles[indexes[i]] === angles[indexes[i + 1]]) { // next one has same angle and is farther
indexes[i] = REMOVED; // remove it logically to avoid O(n) operation to physically remove it
}
}
const hull = [];
for (let i = 0; i < indexes.length; i++) {
const index = indexes[i];
const point = this.points[index];
if (index !== REMOVED) {
if (hull.length < 3) {
hull.push(point);
} else {
while (this.checkOrientation(hull[hull.length - 2], hull[hull.length - 1], point) > 0) {
hull.pop();
}
hull.push(point);
}
}
}
return hull.length < 3 ? [] : hull;
}
/**
* Check the orientation of 3 points in the order given.
*
* It works by comparing the slope of P1->P2 vs P2->P3. If P1->P2 > P2->P3, orientation is clockwise; if
* P1->P2 < P2->P3, counter-clockwise. If P1->P2 == P2->P3, points are co-linear.
*
* @param {[Number, Number]} p1
* @param {[Number, Number]} p2
* @param {[Number, Number]} p3
* @return {Number} positive if orientation is clockwise, negative if counter-clockwise, 0 if co-linear
*/
checkOrientation(p1, p2, p3) {
return (p2[Y] - p1[Y]) * (p3[X] - p2[X]) - (p3[Y] - p2[Y]) * (p2[X] - p1[X]);
}
/**
* @private
* @param {[Number, Number]} a
* @param {[Number, Number]} b
* @return Number
*/
getAngle(a, b) {
return Math.atan2(b[Y] - a[Y], b[X] - a[X]);
}
/**
* @private
* @param {[Number, Number]} p1
* @param {[Number, Number]} p2
* @return {Number}
*/
euclideanDistanceSquared(p1, p2) {
const a = p2[X] - p1[X];
const b = p2[Y] - p1[Y];
return a * a + b * b;
}
/**
* @private
* @return {[Number, Number]}
*/
preparePivotPoint() {
let pivot = this.points[0];
let pivotIndex = 0;
for (let i = 1; i < this.points.length; i++) {
const point = this.points[i];
if (point[Y] < pivot[Y] || point[Y] === pivot[Y] && point[X] < pivot[X]) {
pivot = point;
pivotIndex = i;
}
}
return pivot;
}
}