diff --git a/src/main/java/com/thealgorithms/geometry/JarvisMarch.java b/src/main/java/com/thealgorithms/geometry/JarvisMarch.java new file mode 100644 index 000000000000..dd4f584807db --- /dev/null +++ b/src/main/java/com/thealgorithms/geometry/JarvisMarch.java @@ -0,0 +1,147 @@ +package com.thealgorithms.geometry; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * This class implements the Jarvis March algorithm (also known as the Gift Wrapping algorithm) + * for computing the convex hull of a set of points in a 2D plane. + * The convex hull is the smallest convex polygon that can enclose all given points. + */ +public final class JarvisMarch { + + private JarvisMarch() { + // Private constructor to prevent instantiation + } + + /** + * Represents a point in 2D space with x and y coordinates. + */ + static class Point { + private double x; + private double y; + + /** + * Constructs a Point with specified x and y coordinates. + * + * @param x the x-coordinate of the point + * @param y the y-coordinate of the point + */ + Point(double x, double y) { + this.x = x; + this.y = y; + } + + public double getX() { + return x; + } + + public double getY() { + return y; + } + + @Override + public boolean equals(Object obj) { + // Check if both references point to the same object + if (this == obj) { + return true; + } + // Check if obj is an instance of Point + if (!(obj instanceof Point)) { + return false; + } + Point other = (Point) obj; + // Compare x and y coordinates for equality + return Double.compare(x, other.x) == 0 && Double.compare(y, other.y) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(x, y); // Generate hash code based on x and y coordinates + } + } + + /** + * Computes the convex hull of a given list of points using the Jarvis March algorithm. + * + * @param points a list of Points for which to compute the convex hull + * @return a list of Points representing the vertices of the convex hull in counter-clockwise order + */ + public static List jarvisMarch(List points) { + List hull = new ArrayList<>(); + + // If there are less than 3 points, a convex hull cannot be formed + if (points.size() < 3) { + return hull; + } + + // Find the leftmost point (with the smallest x-coordinate) + Point leftmost = points.get(0); + for (Point p : points) { + if (p.getX() < leftmost.getX()) { + leftmost = p; // Update leftmost point if a new leftmost point is found + } + } + + Point current = leftmost; // Start from the leftmost point + + do { + hull.add(current); // Add current point to the hull + + Point nextTarget = points.get(0); // Initialize next target as first point in list + + for (Point candidate : points) { + // Skip current point + if (candidate.equals(current)) { + continue; + } + + // Check if candidate makes a left turn or is collinear and farther than nextTarget + if (nextTarget.equals(current) || isLeftTurn(current, nextTarget, candidate) || (isCollinear(current, nextTarget, candidate) && distance(current, candidate) > distance(current, nextTarget))) { + nextTarget = candidate; // Update next target if conditions are met + } + } + + current = nextTarget; // Move to the next target point + + } while (!current.equals(leftmost)); // Continue until we loop back to the starting point + + return hull; // Return the computed convex hull + } + + /** + * Determines whether moving from point A to point B to point C makes a left turn. + * + * @param a the starting point + * @param b the second point + * @param c the third point + * @return true if it makes a left turn, false otherwise + */ + private static boolean isLeftTurn(Point a, Point b, Point c) { + return (b.getX() - a.getX()) * (c.getY() - a.getY()) - (b.getY() - a.getY()) * (c.getX() - a.getX()) > 0; + } + + /** + * Checks whether three points A, B, and C are collinear. + * + * @param a the first point + * @param b the second point + * @param c the third point + * @return true if points are collinear, false otherwise + */ + private static boolean isCollinear(Point a, Point b, Point c) { + return (b.getX() - a.getX()) * (c.getY() - a.getY()) == (b.getY() - a.getY()) * (c.getX() - a.getX()); + } + + /** + * Calculates the Euclidean distance between two points A and B. + * + * @param a the first point + * @param b the second point + * @return the distance between points A and B + */ + private static double distance(Point a, Point b) { + return Math.sqrt(Math.pow(b.getX() - a.getX(), 2) + Math.pow(b.getY() - a.getY(), 2)); + } +} diff --git a/src/test/java/com/thealgorithms/geometry/JarvisMarchTest.java b/src/test/java/com/thealgorithms/geometry/JarvisMarchTest.java new file mode 100644 index 000000000000..d6544ace514d --- /dev/null +++ b/src/test/java/com/thealgorithms/geometry/JarvisMarchTest.java @@ -0,0 +1,71 @@ +package com.thealgorithms.geometry; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Unit tests for the {@link JarvisMarch} class, which implements the Jarvis March algorithm + * for computing the convex hull of a set of points. + */ +class JarvisMarchTest { + + /** + * Tests the equals method of the Point class with an object of a different type. + * It verifies that a Point instance is not equal to a non-Point object. + */ + @Test + void testEqualsMethodWithDifferentType() { + JarvisMarch.Point pointA = new JarvisMarch.Point(1, 1); + String notAPoint = "I am not a Point"; + + // Assert that pointA is not equal to a String object + assertNotEquals(pointA, notAPoint); + } + + /** + * Provides test cases for the convex hull computation. + * Each case consists of a list of input points and the expected convex hull result. + * + * @return a stream of arguments containing input points and expected hull points + */ + private static Stream providePointsForConvexHull() { + return Stream.of( + // Test case 1: Simple triangle + Arguments.of(Arrays.asList(new JarvisMarch.Point(0, 0), new JarvisMarch.Point(1, 1), new JarvisMarch.Point(1, 0)), Arrays.asList(new JarvisMarch.Point(0, 0), new JarvisMarch.Point(1, 1), new JarvisMarch.Point(1, 0))), + // Test case 2: Points with one point inside the hull + Arguments.of(Arrays.asList(new JarvisMarch.Point(1, 1), new JarvisMarch.Point(0, 0), new JarvisMarch.Point(2, 2), new JarvisMarch.Point(3, 1), new JarvisMarch.Point(2, 0)), + Arrays.asList(new JarvisMarch.Point(0, 0), new JarvisMarch.Point(2, 2), new JarvisMarch.Point(3, 1), new JarvisMarch.Point(2, 0))), + // Test case 3: Single point (no hull) + Arguments.of(Arrays.asList(new JarvisMarch.Point(0, 0)), Arrays.asList())); + } + + /** + * Parameterized test for the jarvisMarch method. + * It checks if the actual convex hull computed matches the expected hull + * for various sets of input points. + * + * @param inputPoints a list of points to compute the convex hull from + * @param expectedHull a list of expected points forming the convex hull + */ + @ParameterizedTest + @MethodSource("providePointsForConvexHull") + void testConvexHull(List inputPoints, List expectedHull) { + List actualHull = JarvisMarch.jarvisMarch(inputPoints); + + // Assert that the size of actual hull matches the expected hull size + assertEquals(expectedHull.size(), actualHull.size()); + + // Assert that each point in the expected hull matches the corresponding point in the actual hull + for (int i = 0; i < expectedHull.size(); i++) { + assertEquals(expectedHull.get(i), actualHull.get(i)); + } + } +}