Skip to content
184 changes: 184 additions & 0 deletions src/main/java/com/thealgorithms/geometry/RotatingCalipers.java
Original file line number Diff line number Diff line change
@@ -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<Point> points) {
if (points == null || points.size() < 2) {
throw new IllegalArgumentException("At least two points required for diameter");
}

List<Point> 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<Point> points) {
if (points == null || points.size() < 3) {
throw new IllegalArgumentException("At least three points required for width");
}

List<Point> 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<Point> points) {
if (points == null || points.size() < 3) {
throw new IllegalArgumentException("At least three points required");
}

List<Point> 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<Point> 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;
}
}
150 changes: 150 additions & 0 deletions src/test/java/com/thealgorithms/geometry/RotatingCalipersTest.java
Original file line number Diff line number Diff line change
@@ -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<Point> 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<Point> 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<Point> 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<Point> 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<Point> 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<Point> 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<Point> 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<Point> 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<Point> 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<Point> 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<Point> 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<Point> 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<Point> 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);
}
}
Loading