Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Graham's Scan implementation #123

Merged
merged 11 commits into from
Sep 11, 2017
62 changes: 62 additions & 0 deletions src/algorithms/geometry/graham_scan.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const vectorOp = require('./vector_operations2d.js');

/**
* Given an array P with N points on a two dimensional space,
* the function grahamScan calculates the convex hull of P
* in O(N * log(N)) time and using O(N) space.
* Implemented algorithm: Graham's Scan.
*
* @param An array P with N points, each point should be a literal,
* example: {x:0,y:0}.
* @return An array P' with N' points which belongs to the convex hull of P.
*
* Note: If exists a subset S of P that contains only collinear points
* which are present on the convex hull of P, then only the pair with
* the largest distance will be present on P'.
*/
const grahamScan = function(P) {
if (P.length <= 3) {
return P;
}
preprocessing(P);
const convexHull = [P[0], P[1]];
for (let i = 2; i < P.length; i++) {
let j = convexHull.length;
while (j >= 2 &&
!vectorOp.
isCounterClockwise(convexHull[j-2], convexHull[j-1], P[i])) {
convexHull.pop();
j--;
}
convexHull.push(P[i]);
}
return convexHull;
};

/**
* @param An array P with N points, each point should be a literal,
* @return An array P' with N' points which belongs to the convex hull of P.
*
* Note: After the preprocessing the points are ordered by the angle
* between the vectors pivot -> Pi and (0,1). Where Pi is a point on P.
* On the counter-clockwise direction.
*/
const preprocessing = function(P) {
let pivot = P[0];
for (let i = 1; i < P.length; i++) {
if (pivot.y > P[i].y || (pivot.y === P[i].y && pivot.x > P[i].x)) {
pivot = P[i];
}
}

P.sort(function cmp(a, b) {
const area = vectorOp.parallelogramArea(pivot, a, b);
if (Math.abs(area) < 1e-6) {
return vectorOp.length(vectorOp.newVector(pivot, a))
- vectorOp.length(vectorOp.newVector(pivot, b));
}
return -area;
});
};

module.exports = grahamScan;
78 changes: 78 additions & 0 deletions src/algorithms/geometry/vector_operations2d.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* @param A point a, example: {x : 0,y : 0}
* @param A point b, example: {x : 0,y : 0}
* @return A vector ab, example: {x : 0,y : 0}
*/
const newVector = (a, b) => {
return {x: b.x-a.x, y: b.y-a.y};
};

/**
* @param A vector v, example: {x : 0,y : 0}
* @return The length of v.
*/
const length = v => {
return v.x*v.x + v.y*v.y;
};

/**
* Performs the cross product between two vectors.
* @param A vector object, example: {x : 0,y : 0}
* @param A vector object, example: {x : 0,y : 0}
* @return The result of the cross product between u and v.
*/
const crossProduct = (u, v) => {
return u.x*v.y - u.y*v.x;
};

/**
* Calculates the area of the parallelogram that can be
* generated by the vectors ab and ac.
*
* @param A point a, example: {x : 0,y : 0}
* @param A point b, example: {x : 0,y : 0}
* @param A point c, example: {x : 0,y : 0}
* @return A vector ab, example: {x : 0,y : 0}
* Note: Given that the area of the parallelogram is equal
* to the length of the vector w = (ab)x(ac) times -1, if the
* z coordinate of w is negative. With the right-hand rule
* we can deduce that if the area is negative, then
* the vector ab followed by the vector ac do not performs a
* left-turn.
*/
const parallelogramArea = (a, b, c) => {
return crossProduct(newVector(a, b), newVector(a, c));
};

/**
* @param A point object, example: {x : 0,y : 0}
* @param A point object, example: {x : 0,y : 0}
* @param A point object, example: {x : 0,y : 0}
* @return Returns true if the point c is clockwise with respect
* to the straight-line which contains the vector ab, otherwise returns false.
* Note: This rule is given by the Right-hand rule.
*/
const isClockwise = (a, b, c) => {
return parallelogramArea(a, b, c) < 0;
};

/**
* @param A point object, example: {x : 0,y : 0}
* @param A point object, example: {x : 0,y : 0}
* @param A point object, example: {x : 0,y : 0}
* @return Returns true if the point c is counter-clockwise with respect
* to the straight-line which contains the vector ab, otherwise returns false.
* Note: This rule is given by the Right-hand rule.
*/
const isCounterClockwise = (a, b, c) => {
return parallelogramArea(a, b, c) > 0;
};

module.exports = {
newVector: newVector,
length: length,
crossProduct: crossProduct,
parallelogramArea: parallelogramArea,
isClockwise: isClockwise,
isCounterClockwise: isCounterClockwise
};
78 changes: 78 additions & 0 deletions src/test/algorithms/geometry/graham_scan.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
const grahamScan = require('../../../algorithms/geometry/graham_scan');
const assert = require('assert');

describe('Graham s Scan algorithm', () => {
/* we have to ensure the order before using deepEqual. */
let pointComparison;
before(() => {
pointComparison = function(a, b) {
return (a.x != b.x) ? a.x < b.x : a.y < b.y;
};
});

it('ConvexHull of set 0', () => {
const P = [{x: 3, y: 4}, {x: 5, y: 2}, {x: 7, y: 4},
{x: 3, y: 5}, {x: 4.7, y: 3.66}, {x: 5.4, y: 4.52},
{x: 5, y: 6}, {x: 6, y: 6}, {x: 5.62, y: 3.5}];
const convexHullOfP = [{x: 3, y: 4}, {x: 5, y: 2}, {x: 7, y: 4},
{x: 3, y: 5}, {x: 5, y: 6}, {x: 6, y: 6}];
convexHullOfP.sort(pointComparison);
const computedConvexHull = grahamScan(P);
computedConvexHull.sort(pointComparison);
assert.deepEqual(computedConvexHull, convexHullOfP);
});

it('ConvexHull of set 1', () => {
const P = [{x: 2.8, y: 3.6}, {x: 4.2, y: 3.7}, {x: 3.6, y: 4.3},
{x: 2.2, y: 5.2}, {x: 3.6, y: 5.8}, {x: 4.6, y: 5.2},
{x: 4.8, y: 4.5}, {x: 5.6, y: 3.4}, {x: 4, y: 3},
{x: 3, y: 3.1}, {x: 1.9, y: 2.7}, {x: 1.6, y: 3.7},
{x: 2.3, y: 4.2}, {x: 3, y: 2.4}];
const convexHullOfP = [{x: 2.2, y: 5.2}, {x: 3.6, y: 5.8},
{x: 4.6, y: 5.2}, {x: 5.6, y: 3.4},
{x: 1.9, y: 2.7}, {x: 1.6, y: 3.7},
{x: 3, y: 2.4}];
convexHullOfP.sort(pointComparison);
const computedConvexHull = grahamScan(P);
computedConvexHull.sort(pointComparison);
assert.deepEqual(computedConvexHull, convexHullOfP);
});

it('ConvexHull of set 2', () => {
const P = [{x: 1.7, y: 2.4}, {x: 2.8, y: 2}, {x: 1.3, y: 1.6},
{x: 2.4, y: 1.4}, {x: 2.4, y: 0.6}, {x: 1.6, y: 0.7},
{x: 4, y: 1}, {x: 4.9, y: 1.3}, {x: 3.5, y: 2.1},
{x: 3.7, y: 2.8}, {x: 2.6, y: 3.1}, {x: 0.7, y: 2.8},
{x: 0.9, y: 1.6}];
const convexHullOfP = [{x: 2.4, y: 0.6}, {x: 1.6, y: 0.7},
{x: 4, y: 1}, {x: 4.9, y: 1.3},
{x: 3.7, y: 2.8}, {x: 2.6, y: 3.1},
{x: 0.7, y: 2.8}, {x: 0.9, y: 1.6}];
convexHullOfP.sort(pointComparison);
const computedConvexHull = grahamScan(P);
computedConvexHull.sort(pointComparison);
assert.deepEqual(computedConvexHull, convexHullOfP);
});

it('ConvexHull of set 3', () => {
const P = [{x: 2, y: 1}, {x: 3, y: 2}, {x: 4, y: 3}, {x: 5, y: 4},
{x: 3, y: 3}, {x: 3, y: 4}, {x: 2.4, y: 3.5}, {x: 3.6, y: 3.5},
{x: 2, y: 4}, {x: 2, y: 3}, {x: 2, y: 2}];
const convexHullOfP = [{x: 2, y: 1}, {x: 5, y: 4}, {x: 2, y: 4}];
convexHullOfP.sort(pointComparison);
const computedConvexHull = grahamScan(P);
computedConvexHull.sort(pointComparison);
assert.deepEqual(computedConvexHull, convexHullOfP);
});

it('ConvexHull of set 4', () => {
const P = [{x: 2, y: 1}, {x: 3, y: 2}, {x: 4, y: 3}, {x: 4, y: 4},
{x: 2, y: 5}, {x: 2, y: 6}, {x: 2, y: 4}, {x: 5, y: 4},
{x: 3, y: 5}, {x: 2, y: 3}, {x: 2, y: 2}];
const convexHullOfP = [{x: 2, y: 1}, {x: 5, y: 4}, {x: 2, y: 6}];
convexHullOfP.sort(pointComparison);
const computedConvexHull = grahamScan(P);
computedConvexHull.sort(pointComparison);
assert.deepEqual(computedConvexHull, convexHullOfP);
});
});
39 changes: 39 additions & 0 deletions src/test/algorithms/geometry/vector_operations2d.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const vectorOp = require('../../../algorithms/geometry/vector_operations2d');
const assert = require('assert');

describe('VectorOperations', () => {
it('newVector: {0,4}->{1,2}', () => {
assert.deepEqual(vectorOp.
newVector({x: 0, y: 4}, {x: 1, y: 2}), {x: 1, y: -2});
});

it('crossProduct: {-1,7}x{-5,8}', () => {
assert.equal(vectorOp.
crossProduct({x: -1, y: 7}, {x: -5, y: 8}), 27);
});

it('crossProduct: {45,45}x{-45,-45}', () => {
assert.equal(vectorOp.
crossProduct({x: 45, y: 45}, {x: -45, y: -45}), 0);
});

it('crossProduct: {1,1}x{1,0}', () => {
assert.equal(vectorOp.
crossProduct({x: 1, y: 1}, {x: 1, y: 0}), -1);
});

it('isClockwise: {0,0},{2,2},{0,2}', () => {
assert.equal(vectorOp.
isClockwise({x: 0, y: 0}, {x: 2, y: 2}, {x: 0, y: 2}), false);
});

it('isClockwise: {0,0},{2,2},{0,-2}', () => {
assert.equal(vectorOp.
isClockwise({x: 0, y: 0}, {x: 2, y: 2}, {x: 0, y: -2}), true);
});

it('isClockwise: {0,0},{2,2},{1,1}', () => {
assert.equal(vectorOp.
isClockwise({x: 0, y: 0}, {x: 2, y: 2}, {x: 1, y: 1}), false);
});
});