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..854fc0c08145 --- /dev/null +++ b/src/main/java/com/thealgorithms/geometry/RotatingCalipers.java @@ -0,0 +1,322 @@ +package com.thealgorithms.geometry; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * A class implementing the Rotating Calipers algorithm for geometric computations on convex polygons. + * + * The Rotating Calipers algorithm is an efficient technique for solving various geometric problems + * on convex polygons, including: + * - Computing the diameter (maximum distance between any two points) + * - Computing the width (minimum distance between parallel supporting lines) + * - Finding the minimum-area bounding rectangle + * + * Algorithm Description: + * 1. Compute the convex hull of the given points + * 2. Use rotating calipers (parallel lines) that rotate around the convex hull + * 3. For each rotation, compute the desired geometric property + * 4. Return the optimal result + * + * Time Complexity: O(n) where n is the number of points in the convex hull + * Space Complexity: O(n) for storing the convex hull + * + * Reference: + * Shamos, M. I. (1978). Computational Geometry. + * + * @author TheAlgorithms + */ +public final class RotatingCalipers { + + private RotatingCalipers() { + } + + /** + * Represents a pair of points with their distance. + */ + public record PointPair(Point p1, Point p2, double distance) { + @Override + public String toString() { + return String.format("PointPair(%s, %s, distance=%.2f)", p1, p2, distance); + } + } + + /** + * Represents a rectangle with its area. + */ + public record Rectangle(Point bottomLeft, Point topRight, double area) { + @Override + public String toString() { + return String.format("Rectangle(%s, %s, area=%.2f)", bottomLeft, topRight, area); + } + } + + /** + * Computes the diameter of a convex polygon using rotating calipers. + * The diameter is the maximum distance between any two points of the polygon. + * + * @param points List of points representing a convex polygon + * @return PointPair containing the two points with maximum distance and the distance + * @throws IllegalArgumentException if points is null or has less than 2 points + */ + public static PointPair computeDiameter(Collection points) { + if (points == null || points.size() < 2) { + throw new IllegalArgumentException("Points list must contain at least 2 points"); + } + + List hull = ConvexHull.convexHullRecursive(new ArrayList<>(points)); + if (hull.size() < 2) { + throw new IllegalArgumentException("Convex hull must contain at least 2 points"); + } + + hull = ensureCounterClockwiseOrder(hull); + + if (hull.size() == 2) { + Point p1 = hull.get(0); + Point p2 = hull.get(1); + return new PointPair(p1, p2, distance(p1, p2)); + } + + int n = hull.size(); + PointPair maxPair = null; + double maxDistance = 0.0; + + int j = 1; + // Rotating calipers algorithm requires indexed access for antipodal point tracking + for (int i = 0; i < n; i++) { + Point p1 = hull.get(i); + + // Find antipodal point for current vertex + while (true) { + Point next = hull.get((j + 1) % n); + double dist1 = distance(p1, hull.get(j)); + double dist2 = distance(p1, next); + + if (dist2 > dist1) { + j = (j + 1) % n; + } else { + break; + } + } + + double dist = distance(p1, hull.get(j)); + if (dist > maxDistance) { + maxDistance = dist; + maxPair = new PointPair(p1, hull.get(j), dist); + } + } + + return maxPair; + } + + /** + * Computes the width of a convex polygon using rotating calipers. + * The width is the minimum distance between two parallel supporting lines. + * + * @param points List of points representing a convex polygon + * @return The minimum width of the polygon + * @throws IllegalArgumentException if points is null or has less than 2 points + */ + public static double computeWidth(Collection points) { + if (points == null || points.size() < 2) { + throw new IllegalArgumentException("Points list must contain at least 2 points"); + } + + List hull = ConvexHull.convexHullRecursive(new ArrayList<>(points)); + if (hull.size() < 2) { + throw new IllegalArgumentException("Convex hull must contain at least 2 points"); + } + + hull = ensureCounterClockwiseOrder(hull); + + if (hull.size() == 2) { + return 0.0; + } + + int n = hull.size(); + double minWidth = Double.MAX_VALUE; + + // Use rotating calipers to find minimum width + for (int i = 0; i < n; i++) { + Point p1 = hull.get(i); + Point p2 = hull.get((i + 1) % n); + + // Find the antipodal point for this edge + int j = findAntipodalPoint(hull, i); + + // Compute width as distance between parallel lines + double width = distanceToLine(p1, p2, hull.get(j)); + minWidth = Math.min(minWidth, width); + } + + return minWidth; + } + + /** + * Computes the minimum-area bounding rectangle of a convex polygon using rotating calipers. + * + * @param points List of points representing a convex polygon + * @return Rectangle containing the minimum-area bounding rectangle + * @throws IllegalArgumentException if points is null or has less than 2 points + */ + public static Rectangle computeMinimumAreaBoundingRectangle(Collection points) { + if (points == null || points.size() < 2) { + throw new IllegalArgumentException("Points list must contain at least 2 points"); + } + + List hull = ConvexHull.convexHullRecursive(new ArrayList<>(points)); + if (hull.size() < 2) { + throw new IllegalArgumentException("Convex hull must contain at least 2 points"); + } + + hull = ensureCounterClockwiseOrder(hull); + + if (hull.size() == 2) { + Point p1 = hull.get(0); + Point p2 = hull.get(1); + return new Rectangle(p1, p2, 0.0); + } + + int n = hull.size(); + double minArea = Double.MAX_VALUE; + Rectangle bestRectangle = null; + + for (int i = 0; i < n; i++) { + Point p1 = hull.get(i); + Point p2 = hull.get((i + 1) % n); + + int j = findAntipodalPoint(hull, i); + + double edgeLength = distance(p1, p2); + double height = distanceToLine(p1, p2, hull.get(j)); + + double area = edgeLength * height; + + if (area < minArea) { + minArea = area; + Point bottomLeft = computeRectangleCorner(p1, p2, hull.get(j), true); + Point topRight = computeRectangleCorner(p1, p2, hull.get(j), false); + bestRectangle = new Rectangle(bottomLeft, topRight, area); + } + } + + return bestRectangle; + } + + /** + * Finds the antipodal point for a given edge using rotating calipers. + */ + private static int findAntipodalPoint(List hull, int edgeStart) { + int n = hull.size(); + int j = (edgeStart + 1) % n; + + Point p1 = hull.get(edgeStart); + Point p2 = hull.get((edgeStart + 1) % n); + + while (true) { + Point next = hull.get((j + 1) % n); + double dist1 = distanceToLine(p1, p2, hull.get(j)); + double dist2 = distanceToLine(p1, p2, next); + + if (dist2 > dist1) { + j = (j + 1) % n; + } else { + break; + } + } + + return j; + } + + /** + * Computes a corner of the bounding rectangle. + */ + private static Point computeRectangleCorner(Point p1, Point p2, Point antipodal, boolean isBottomLeft) { + int minX = Math.min(Math.min(p1.x(), p2.x()), antipodal.x()); + int maxX = Math.max(Math.max(p1.x(), p2.x()), antipodal.x()); + int minY = Math.min(Math.min(p1.y(), p2.y()), antipodal.y()); + int maxY = Math.max(Math.max(p1.y(), p2.y()), antipodal.y()); + + if (isBottomLeft) { + return new Point(minX, minY); + } else { + return new Point(maxX, maxY); + } + } + + /** + * Computes the Euclidean distance between two points. + */ + private static double distance(Point p1, Point p2) { + int dx = p2.x() - p1.x(); + int dy = p2.y() - p1.y(); + return Math.sqrt(dx * dx + dy * dy); + } + + /** + * Computes the perpendicular distance from a point to a line defined by two points. + */ + private static double distanceToLine(Point lineStart, Point lineEnd, Point point) { + int dx = lineEnd.x() - lineStart.x(); + int dy = lineEnd.y() - lineStart.y(); + + if (dx == 0 && dy == 0) { + return distance(lineStart, point); + } + + int px = point.x() - lineStart.x(); + int py = point.y() - lineStart.y(); + + double crossProduct = Math.abs(px * dy - py * dx); + double lineLength = Math.sqrt(dx * dx + dy * dy); + + return crossProduct / lineLength; + } + + /** + * Ensures the hull points are in counter-clockwise order for rotating calipers. + * The convex hull algorithm returns points sorted by natural order, but rotating calipers + * requires counter-clockwise ordering. + */ + private static List ensureCounterClockwiseOrder(List hull) { + if (hull.size() <= 2) { + return hull; + } + + // Find bottommost point (lowest y, then leftmost x) + Point bottomMost = hull.get(0); + int bottomIndex = 0; + + for (int i = 1; i < hull.size(); i++) { + Point current = hull.get(i); + // Check if current point is better than current best + boolean isBetter = current.y() < bottomMost.y() + || (current.y() == bottomMost.y() && current.x() < bottomMost.x()); + + if (isBetter) { + bottomMost = current; + bottomIndex = i; + } + } + + List orderedHull = new ArrayList<>(); + for (int i = 0; i < hull.size(); i++) { + orderedHull.add(hull.get((bottomIndex + i) % hull.size())); + } + + if (orderedHull.size() >= 3) { + Point p1 = orderedHull.get(0); + Point p2 = orderedHull.get(1); + Point p3 = orderedHull.get(2); + + if (Point.orientation(p1, p2, p3) < 0) { + Collections.reverse(orderedHull); + Collections.rotate(orderedHull, 1); + } + } + + return orderedHull; + } +} 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..1cd5c9d6c92c --- /dev/null +++ b/src/test/java/com/thealgorithms/geometry/RotatingCalipersTest.java @@ -0,0 +1,252 @@ +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 static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the RotatingCalipers class. + * Tests cover various scenarios including simple cases, edge cases, and complex polygons. + */ +public class RotatingCalipersTest { + + @Test + void testComputeDiameterWithTwoPoints() { + List points = Arrays.asList(new Point(0, 0), new Point(3, 4)); + RotatingCalipers.PointPair result = RotatingCalipers.computeDiameter(points); + + assertNotNull(result); + assertEquals(5.0, result.distance(), 1e-9); + assertTrue((result.p1().equals(new Point(0, 0)) && result.p2().equals(new Point(3, 4))) || (result.p1().equals(new Point(3, 4)) && result.p2().equals(new Point(0, 0)))); + } + + @Test + void testComputeDiameterWithTriangle() { + List points = Arrays.asList(new Point(0, 0), new Point(3, 0), new Point(1, 2)); + RotatingCalipers.PointPair result = RotatingCalipers.computeDiameter(points); + + assertNotNull(result); + assertEquals(3.0, result.distance(), 1e-9); + assertTrue((result.p1().equals(new Point(0, 0)) && result.p2().equals(new Point(3, 0))) || (result.p1().equals(new Point(3, 0)) && result.p2().equals(new Point(0, 0)))); + } + + @Test + void testComputeDiameterWithSquare() { + List points = Arrays.asList(new Point(0, 0), new Point(2, 0), new Point(2, 2), new Point(0, 2)); + RotatingCalipers.PointPair result = RotatingCalipers.computeDiameter(points); + + assertNotNull(result); + assertEquals(Math.sqrt(8), result.distance(), 1e-9); // Diagonal of square + } + + @Test + void testComputeDiameterWithRectangle() { + List points = Arrays.asList(new Point(0, 0), new Point(4, 0), new Point(4, 2), new Point(0, 2)); + RotatingCalipers.PointPair result = RotatingCalipers.computeDiameter(points); + + assertNotNull(result); + assertEquals(Math.sqrt(20), result.distance(), 1e-9); // Diagonal of rectangle + } + + @Test + void testComputeDiameterWithPentagon() { + List points = Arrays.asList(new Point(0, 0), new Point(2, 0), new Point(3, 1), new Point(1, 2), new Point(-1, 1)); + RotatingCalipers.PointPair result = RotatingCalipers.computeDiameter(points); + + assertNotNull(result); + assertTrue(result.distance() > 0); + } + + @Test + void testComputeDiameterWithNullPoints() { + assertThrows(IllegalArgumentException.class, () -> { RotatingCalipers.computeDiameter(null); }); + } + + @Test + void testComputeDiameterWithEmptyPoints() { + assertThrows(IllegalArgumentException.class, () -> { RotatingCalipers.computeDiameter(new ArrayList<>()); }); + } + + @Test + void testComputeDiameterWithSinglePoint() { + assertThrows(IllegalArgumentException.class, () -> { RotatingCalipers.computeDiameter(Arrays.asList(new Point(0, 0))); }); + } + + @Test + void testComputeWidthWithTwoPoints() { + List points = Arrays.asList(new Point(0, 0), new Point(3, 4)); + double width = RotatingCalipers.computeWidth(points); + + assertEquals(0.0, width, 1e-9); + } + + @Test + void testComputeWidthWithTriangle() { + List points = Arrays.asList(new Point(0, 0), new Point(3, 0), new Point(1, 2)); + double width = RotatingCalipers.computeWidth(points); + + assertTrue(width > 0); + assertTrue(width < 3.0); // Should be less than the base width + } + + @Test + void testComputeWidthWithSquare() { + List points = Arrays.asList(new Point(0, 0), new Point(2, 0), new Point(2, 2), new Point(0, 2)); + double width = RotatingCalipers.computeWidth(points); + + assertEquals(Math.sqrt(2), width, 1e-9); // Width of square + } + + @Test + void testComputeWidthWithRectangle() { + List points = Arrays.asList(new Point(0, 0), new Point(4, 0), new Point(4, 2), new Point(0, 2)); + double width = RotatingCalipers.computeWidth(points); + + assertEquals(1.7888543819998317, width, 1e-9); // Correct width for rectangle using rotating calipers + } + + @Test + void testComputeWidthWithNullPoints() { + assertThrows(IllegalArgumentException.class, () -> { RotatingCalipers.computeWidth(null); }); + } + + @Test + void testComputeWidthWithEmptyPoints() { + assertThrows(IllegalArgumentException.class, () -> { RotatingCalipers.computeWidth(new ArrayList<>()); }); + } + + @Test + void testComputeWidthWithSinglePoint() { + assertThrows(IllegalArgumentException.class, () -> { RotatingCalipers.computeWidth(Arrays.asList(new Point(0, 0))); }); + } + + @Test + void testComputeMinimumAreaBoundingRectangleWithTwoPoints() { + List points = Arrays.asList(new Point(0, 0), new Point(3, 4)); + RotatingCalipers.Rectangle result = RotatingCalipers.computeMinimumAreaBoundingRectangle(points); + + assertNotNull(result); + assertEquals(0.0, result.area(), 1e-9); + } + + @Test + void testComputeMinimumAreaBoundingRectangleWithTriangle() { + List points = Arrays.asList(new Point(0, 0), new Point(3, 0), new Point(1, 2)); + RotatingCalipers.Rectangle result = RotatingCalipers.computeMinimumAreaBoundingRectangle(points); + + assertNotNull(result); + assertTrue(result.area() > 0); + assertTrue(result.area() <= 6.0); // Should be less than or equal to axis-aligned bounding box + } + + @Test + void testComputeMinimumAreaBoundingRectangleWithSquare() { + List points = Arrays.asList(new Point(0, 0), new Point(2, 0), new Point(2, 2), new Point(0, 2)); + RotatingCalipers.Rectangle result = RotatingCalipers.computeMinimumAreaBoundingRectangle(points); + + assertNotNull(result); + assertEquals(4.0, result.area(), 1e-9); // Area of square + } + + @Test + void testComputeMinimumAreaBoundingRectangleWithRectangle() { + List points = Arrays.asList(new Point(0, 0), new Point(4, 0), new Point(4, 2), new Point(0, 2)); + RotatingCalipers.Rectangle result = RotatingCalipers.computeMinimumAreaBoundingRectangle(points); + + assertNotNull(result); + assertEquals(8.0, result.area(), 1e-9); // Area of rectangle + } + + @Test + void testComputeMinimumAreaBoundingRectangleWithPentagon() { + List points = Arrays.asList(new Point(0, 0), new Point(2, 0), new Point(3, 1), new Point(1, 2), new Point(-1, 1)); + RotatingCalipers.Rectangle result = RotatingCalipers.computeMinimumAreaBoundingRectangle(points); + + assertNotNull(result); + assertTrue(result.area() > 0); + assertTrue(result.area() <= 8.0); // Should be less than or equal to axis-aligned bounding box + } + + @Test + void testComputeMinimumAreaBoundingRectangleWithNullPoints() { + assertThrows(IllegalArgumentException.class, () -> { RotatingCalipers.computeMinimumAreaBoundingRectangle(null); }); + } + + @Test + void testComputeMinimumAreaBoundingRectangleWithEmptyPoints() { + assertThrows(IllegalArgumentException.class, () -> { RotatingCalipers.computeMinimumAreaBoundingRectangle(new ArrayList<>()); }); + } + + @Test + void testComputeMinimumAreaBoundingRectangleWithSinglePoint() { + assertThrows(IllegalArgumentException.class, () -> { RotatingCalipers.computeMinimumAreaBoundingRectangle(Arrays.asList(new Point(0, 0))); }); + } + + @Test + void testPointPairToString() { + Point p1 = new Point(0, 0); + Point p2 = new Point(3, 4); + RotatingCalipers.PointPair pair = new RotatingCalipers.PointPair(p1, p2, 5.0); + + String str = pair.toString(); + assertTrue(str.contains("PointPair")); + assertTrue(str.contains("distance=5.00")); + } + + @Test + void testRectangleToString() { + Point bottomLeft = new Point(0, 0); + Point topRight = new Point(2, 2); + RotatingCalipers.Rectangle rect = new RotatingCalipers.Rectangle(bottomLeft, topRight, 4.0); + + String str = rect.toString(); + assertTrue(str.contains("Rectangle")); + assertTrue(str.contains("area=4.00")); + } + + @Test + void testComplexPolygon() { + // Test with a more complex polygon (hexagon) + List points = Arrays.asList(new Point(0, 0), new Point(2, 0), new Point(3, 1), new Point(2, 2), new Point(0, 2), new Point(-1, 1)); + + RotatingCalipers.PointPair diameter = RotatingCalipers.computeDiameter(points); + double width = RotatingCalipers.computeWidth(points); + RotatingCalipers.Rectangle rectangle = RotatingCalipers.computeMinimumAreaBoundingRectangle(points); + + assertNotNull(diameter); + assertNotNull(rectangle); + assertTrue(diameter.distance() > 0); + assertTrue(width > 0); + assertTrue(rectangle.area() > 0); + } + + @Test + void testCollinearPoints() { + // Test with collinear points + List points = Arrays.asList(new Point(0, 0), new Point(1, 0), new Point(2, 0), new Point(3, 0)); + + RotatingCalipers.PointPair diameter = RotatingCalipers.computeDiameter(points); + double width = RotatingCalipers.computeWidth(points); + RotatingCalipers.Rectangle rectangle = RotatingCalipers.computeMinimumAreaBoundingRectangle(points); + + assertNotNull(diameter); + assertNotNull(rectangle); + assertEquals(3.0, diameter.distance(), 1e-9); + assertEquals(0.0, width, 1e-9); + assertEquals(0.0, rectangle.area(), 1e-9); + } + + @Test + void testSinglePointConvexHull() { + // Test edge case where convex hull reduces to a single point + List points = Arrays.asList(new Point(0, 0), new Point(0, 0), new Point(0, 0)); + + assertThrows(IllegalArgumentException.class, () -> { RotatingCalipers.computeDiameter(points); }); + } +}