diff --git a/src/main/java/com/thealgorithms/geometry/RotatingCalipers.java b/src/main/java/com/thealgorithms/geometry/RotatingCalipers.java new file mode 100644 index 000000000000..13acb958e7da --- /dev/null +++ b/src/main/java/com/thealgorithms/geometry/RotatingCalipers.java @@ -0,0 +1,184 @@ +package com.thealgorithms.geometry; + +import java.util.Collections; +import java.util.List; + +/** + * Rotating Calipers algorithm to compute: + * - Diameter of a convex polygon + * - Width of a convex polygon + * - Minimum-area bounding rectangle + */ +public final class RotatingCalipers { + + private RotatingCalipers() { + } + + // -------------------- Inner Classes -------------------- + public record PointPair(Point p1, Point p2, double distance) { + } + + public record Rectangle(Point[] corners, double width, double height, double area) { + } + + // -------------------- Diameter -------------------- + public static PointPair diameter(List points) { + if (points == null || points.size() < 2) { + throw new IllegalArgumentException("At least two points required for diameter"); + } + + List hull = ConvexHull.convexHullRecursive(points); + orderCounterClockwise(hull); + + double maxDist = 0; + Point bestA = hull.get(0); + Point bestB = hull.get(0); + int n = hull.size(); + int j = 1; + + for (Point a : hull) { + while (true) { + Point b1 = hull.get(j); + Point b2 = hull.get((j + 1) % n); + double d1 = distanceSquared(a, b1); + double d2 = distanceSquared(a, b2); + if (d2 > d1) { + j = (j + 1) % n; + } else { + break; + } + } + double d = distanceSquared(a, hull.get(j)); + if (d > maxDist) { + maxDist = d; + bestA = a; + bestB = hull.get(j); + } + } + + return new PointPair(bestA, bestB, Math.sqrt(maxDist)); + } + + // -------------------- Width -------------------- + public static double width(List points) { + if (points == null || points.size() < 3) { + throw new IllegalArgumentException("At least three points required for width"); + } + + List hull = ConvexHull.convexHullRecursive(points); + orderCounterClockwise(hull); + + double minWidth = Double.MAX_VALUE; + int n = hull.size(); + + for (int i = 0; i < n; i++) { + Point a = hull.get(i); + Point b = hull.get((i + 1) % n); + + double ux = b.x() - a.x(); + double uy = b.y() - a.y(); + double len = Math.hypot(ux, uy); + ux /= len; + uy /= len; + + double vx = -uy; + double vy = ux; + + double minProjV = Double.MAX_VALUE; + double maxProjV = -Double.MAX_VALUE; + for (Point p : hull) { + double projV = p.x() * vx + p.y() * vy; + minProjV = Math.min(minProjV, projV); + maxProjV = Math.max(maxProjV, projV); + } + minWidth = Math.min(minWidth, maxProjV - minProjV); + } + + return minWidth; + } + + // -------------------- Minimum-Area Bounding Rectangle -------------------- + public static Rectangle minAreaBoundingRectangle(List points) { + if (points == null || points.size() < 3) { + throw new IllegalArgumentException("At least three points required"); + } + + List hull = ConvexHull.convexHullRecursive(points); + orderCounterClockwise(hull); + + double minArea = Double.MAX_VALUE; + Point[] bestCorners = null; + double bestWidth = 0; + double bestHeight = 0; + int n = hull.size(); + + for (int i = 0; i < n; i++) { + Point a = hull.get(i); + Point b = hull.get((i + 1) % n); + + double edgeDx = b.x() - a.x(); + double edgeDy = b.y() - a.y(); + double edgeLen = Math.hypot(edgeDx, edgeDy); + double ux = edgeDx / edgeLen; + double uy = edgeDy / edgeLen; + double vx = -uy; + double vy = ux; + + double minU = Double.MAX_VALUE; + double maxU = -Double.MAX_VALUE; + double minV = Double.MAX_VALUE; + double maxV = -Double.MAX_VALUE; + + for (Point p : hull) { + double projU = p.x() * ux + p.y() * uy; + double projV = p.x() * vx + p.y() * vy; + if (projU < minU) { + minU = projU; + } + if (projU > maxU) { + maxU = projU; + } + if (projV < minV) { + minV = projV; + } + if (projV > maxV) { + maxV = projV; + } + } + + double width = maxU - minU; + double height = maxV - minV; + double area = width * height; + + if (area < minArea) { + minArea = area; + bestWidth = width; + bestHeight = height; + bestCorners = new Point[] {new Point((int) (ux * minU + vx * minV), (int) (uy * minU + vy * minV)), new Point((int) (ux * maxU + vx * minV), (int) (uy * maxU + vy * minV)), new Point((int) (ux * maxU + vx * maxV), (int) (uy * maxU + vy * maxV)), + new Point((int) (ux * minU + vx * maxV), (int) (uy * minU + vy * maxV))}; + } + } + + return new Rectangle(bestCorners, bestWidth, bestHeight, minArea); + } + + // -------------------- Helper Methods -------------------- + private static void orderCounterClockwise(List points) { + double area = 0.0; + int n = points.size(); + for (int i = 0; i < n; i++) { + Point a = points.get(i); + Point b = points.get((i + 1) % n); + area += (a.x() * b.y()) - (b.x() * a.y()); + } + if (area < 0) { + Collections.reverse(points); + } + } + + private static double distanceSquared(Point a, Point b) { + double dx = a.x() - b.x(); + double dy = a.y() - b.y(); + return dx * dx + dy * dy; + } +} diff --git a/src/test/java/com/thealgorithms/geometry/RotatingCalipersTest.java b/src/test/java/com/thealgorithms/geometry/RotatingCalipersTest.java new file mode 100644 index 000000000000..5c051c01df5e --- /dev/null +++ b/src/test/java/com/thealgorithms/geometry/RotatingCalipersTest.java @@ -0,0 +1,150 @@ +package com.thealgorithms.geometry; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; + +public class RotatingCalipersTest { + + @Test + void testDiameterSimpleTriangle() { + List convexHull = Arrays.asList(new Point(0, 0), new Point(4, 0), new Point(2, 3)); + RotatingCalipers.PointPair result = RotatingCalipers.diameter(convexHull); + + assertNotNull(result); + assertEquals(4.0, result.distance(), 0.001); + } + + @Test + void testDiameterSquare() { + List convexHull = Arrays.asList(new Point(0, 0), new Point(3, 0), new Point(3, 3), new Point(0, 3)); + RotatingCalipers.PointPair result = RotatingCalipers.diameter(convexHull); + + assertNotNull(result); + assertEquals(Math.sqrt(18), result.distance(), 0.001); + } + + @Test + void testDiameterComplexPolygon() { + List convexHull = Arrays.asList(new Point(0, 0), new Point(3, 0), new Point(3, 3), new Point(0, 3)); + RotatingCalipers.PointPair result = RotatingCalipers.diameter(convexHull); + + assertNotNull(result); + assertEquals(Math.sqrt(18), result.distance(), 0.001); + } + + @Test + void testDiameterTwoPoints() { + List convexHull = Arrays.asList(new Point(0, 0), new Point(5, 0)); + RotatingCalipers.PointPair result = RotatingCalipers.diameter(convexHull); + + assertNotNull(result); + assertEquals(5.0, result.distance(), 0.001); + assertEquals(new Point(0, 0), result.p1()); + assertEquals(new Point(5, 0), result.p2()); + } + + @Test + void testDiameterInvalidInput() { + assertThrows(IllegalArgumentException.class, () -> RotatingCalipers.diameter(null)); + assertThrows(IllegalArgumentException.class, () -> RotatingCalipers.diameter(Arrays.asList())); + assertThrows(IllegalArgumentException.class, () -> RotatingCalipers.diameter(Arrays.asList(new Point(0, 0)))); + } + + @Test + void testWidthSimpleTriangle() { + List convexHull = Arrays.asList(new Point(0, 0), new Point(4, 0), new Point(2, 3)); + double result = RotatingCalipers.width(convexHull); + + assertEquals(3, result, 0.1); + } + + @Test + void testWidthSquare() { + List convexHull = Arrays.asList(new Point(0, 0), new Point(3, 0), new Point(3, 3), new Point(0, 3)); + double result = RotatingCalipers.width(convexHull); + + assertEquals(3.0, result, 0.001); + } + + @Test + void testWidthRectangle() { + List convexHull = Arrays.asList(new Point(0, 0), new Point(5, 0), new Point(5, 2), new Point(0, 2)); + double result = RotatingCalipers.width(convexHull); + + assertEquals(2.0, result, 0.001); + } + + @Test + void testWidthInvalidInput() { + assertThrows(IllegalArgumentException.class, () -> RotatingCalipers.width(null)); + assertThrows(IllegalArgumentException.class, () -> RotatingCalipers.width(Arrays.asList())); + assertThrows(IllegalArgumentException.class, () -> RotatingCalipers.width(Arrays.asList(new Point(0, 0), new Point(1, 1)))); + } + + @Test + void testMinAreaBoundingRectangleSquare() { + List convexHull = Arrays.asList(new Point(0, 0), new Point(3, 0), new Point(3, 3), new Point(0, 3)); + RotatingCalipers.Rectangle result = RotatingCalipers.minAreaBoundingRectangle(convexHull); + + assertNotNull(result); + assertEquals(9.0, result.area(), 0.1); + assertEquals(3.0, result.width(), 0.1); + assertEquals(3.0, result.height(), 0.1); + } + + @Test + void testMinAreaBoundingRectangleTriangle() { + List convexHull = Arrays.asList(new Point(0, 0), new Point(4, 0), new Point(2, 3)); + RotatingCalipers.Rectangle result = RotatingCalipers.minAreaBoundingRectangle(convexHull); + + assertNotNull(result); + assertNotNull(result.corners()); + assertEquals(4, result.corners().length); + } + + @Test + void testMinAreaBoundingRectangleRectangle() { + List convexHull = Arrays.asList(new Point(0, 0), new Point(5, 0), new Point(5, 2), new Point(0, 2)); + RotatingCalipers.Rectangle result = RotatingCalipers.minAreaBoundingRectangle(convexHull); + + assertNotNull(result); + assertEquals(10.0, result.area(), 0.1); + } + + @Test + void testMinAreaBoundingRectangleInvalidInput() { + assertThrows(IllegalArgumentException.class, () -> RotatingCalipers.minAreaBoundingRectangle(null)); + assertThrows(IllegalArgumentException.class, () -> RotatingCalipers.minAreaBoundingRectangle(Arrays.asList())); + assertThrows(IllegalArgumentException.class, () -> RotatingCalipers.minAreaBoundingRectangle(Arrays.asList(new Point(0, 0), new Point(1, 1)))); + } + + @Test + void testDiameterWithLargeConvexHull() { + List convexHull = Arrays.asList(new Point(0, 0), new Point(3, 0), new Point(3, 3), new Point(0, 3), new Point(2, -4), new Point(1, -3)); + RotatingCalipers.PointPair result = RotatingCalipers.diameter(convexHull); + + assertNotNull(result); + } + + @Test + void testWidthWithLargeConvexHull() { + List convexHull = Arrays.asList(new Point(0, 0), new Point(3, 0), new Point(3, 3), new Point(0, 3)); + double result = RotatingCalipers.width(convexHull); + + assertEquals(3.0, result, 0.001); + } + + @Test + void testMinAreaBoundingRectangleWithLargeConvexHull() { + List convexHull = Arrays.asList(new Point(0, 0), new Point(10, 0), new Point(10, 5), new Point(0, 5)); + RotatingCalipers.Rectangle result = RotatingCalipers.minAreaBoundingRectangle(convexHull); + + assertNotNull(result); + assertEquals(50.0, result.area(), 0.1); + } +}