Skip to content

Commit 4fca4f1

Browse files
committed
feat: added rotating calipers
1 parent 9484c7e commit 4fca4f1

File tree

2 files changed

+264
-0
lines changed

2 files changed

+264
-0
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package com.thealgorithms.geometry;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.List;
6+
7+
/**
8+
* Rotating Calipers algorithm to compute:
9+
* - Diameter of a convex polygon
10+
* - Width of a convex polygon
11+
* - Minimum-area bounding rectangle
12+
*/
13+
public final class RotatingCalipers {
14+
15+
private RotatingCalipers() {
16+
}
17+
18+
// -------------------- Inner Classes --------------------
19+
public static record PointPair(Point p1, Point p2, double distance) {
20+
}
21+
22+
public static record Rectangle(PointD[] corners, double width, double height, double area) {
23+
}
24+
25+
// Double-based point for precise rectangle corners
26+
public static record PointD(double x, double y) {
27+
}
28+
29+
// -------------------- Diameter --------------------
30+
public static PointPair diameter(List<Point> points) {
31+
if (points == null || points.size() < 2) {
32+
throw new IllegalArgumentException("At least two points required for diameter");
33+
}
34+
35+
List<Point> hull = ConvexHull.convexHullRecursive(points);
36+
orderCounterClockwise(hull);
37+
int n = hull.size();
38+
39+
double maxDist = 0;
40+
Point bestA = hull.get(0), bestB = hull.get(0);
41+
42+
int j = 1;
43+
for (int i = 0; i < n; i++) {
44+
Point a = hull.get(i);
45+
while (true) {
46+
Point b1 = hull.get(j);
47+
Point b2 = hull.get((j + 1) % n);
48+
double d1 = distanceSquared(a, b1);
49+
double d2 = distanceSquared(a, b2);
50+
if (d2 > d1) {
51+
j = (j + 1) % n;
52+
} else {
53+
break;
54+
}
55+
}
56+
double d = distanceSquared(a, hull.get(j));
57+
if (d > maxDist) {
58+
maxDist = d;
59+
bestA = a;
60+
bestB = hull.get(j);
61+
}
62+
}
63+
return new PointPair(bestA, bestB, Math.sqrt(maxDist));
64+
}
65+
66+
// -------------------- Width --------------------
67+
public static double width(List<Point> points) {
68+
if (points == null || points.size() < 3) {
69+
throw new IllegalArgumentException("At least three points required for width");
70+
}
71+
72+
List<Point> hull = ConvexHull.convexHullRecursive(points);
73+
orderCounterClockwise(hull);
74+
int n = hull.size();
75+
76+
double minWidth = Double.MAX_VALUE;
77+
78+
for (int i = 0; i < n; i++) {
79+
Point a = hull.get(i);
80+
Point b = hull.get((i + 1) % n);
81+
82+
double ux = b.x() - a.x();
83+
double uy = b.y() - a.y();
84+
double len = Math.hypot(ux, uy);
85+
ux /= len;
86+
uy /= len;
87+
88+
double vx = -uy;
89+
double vy = ux;
90+
91+
double minProjV = Double.MAX_VALUE;
92+
double maxProjV = -Double.MAX_VALUE;
93+
for (Point p : hull) {
94+
// Project relative to edge starting point 'a'
95+
double projV = (p.x() - a.x()) * vx + (p.y() - a.y()) * vy;
96+
minProjV = Math.min(minProjV, projV);
97+
maxProjV = Math.max(maxProjV, projV);
98+
}
99+
minWidth = Math.min(minWidth, maxProjV - minProjV);
100+
}
101+
return minWidth;
102+
}
103+
104+
// -------------------- Minimum-Area Bounding Rectangle --------------------
105+
public static Rectangle minAreaBoundingRectangle(List<Point> points) {
106+
if (points == null || points.size() < 3) {
107+
throw new IllegalArgumentException("At least three points required");
108+
}
109+
110+
List<Point> hull = ConvexHull.convexHullRecursive(points);
111+
orderCounterClockwise(hull);
112+
int n = hull.size();
113+
114+
double minArea = Double.MAX_VALUE;
115+
PointD[] bestCorners = null;
116+
double bestWidth = 0;
117+
double bestHeight = 0;
118+
119+
for (int i = 0; i < n; i++) {
120+
Point a = hull.get(i);
121+
Point b = hull.get((i + 1) % n);
122+
123+
double edgeDx = b.x() - a.x();
124+
double edgeDy = b.y() - a.y();
125+
double edgeLen = Math.hypot(edgeDx, edgeDy);
126+
double ux = edgeDx / edgeLen;
127+
double uy = edgeDy / edgeLen;
128+
double vx = -uy;
129+
double vy = ux;
130+
131+
double minU = Double.MAX_VALUE, maxU = -Double.MAX_VALUE;
132+
double minV = Double.MAX_VALUE, maxV = -Double.MAX_VALUE;
133+
134+
for (Point p : hull) {
135+
// Project relative to edge 'a'
136+
double projU = (p.x() - a.x()) * ux + (p.y() - a.y()) * uy;
137+
double projV = (p.x() - a.x()) * vx + (p.y() - a.y()) * vy;
138+
minU = Math.min(minU, projU);
139+
maxU = Math.max(maxU, projU);
140+
minV = Math.min(minV, projV);
141+
maxV = Math.max(maxV, projV);
142+
}
143+
144+
double width = maxU - minU;
145+
double height = maxV - minV;
146+
double area = width * height;
147+
148+
if (area < minArea) {
149+
minArea = area;
150+
bestWidth = width;
151+
bestHeight = height;
152+
153+
bestCorners = new PointD[] {new PointD(a.x() + ux * minU + vx * minV, a.y() + uy * minU + vy * minV), new PointD(a.x() + ux * maxU + vx * minV, a.y() + uy * maxU + vy * minV), new PointD(a.x() + ux * maxU + vx * maxV, a.y() + uy * maxU + vy * maxV),
154+
new PointD(a.x() + ux * minU + vx * maxV, a.y() + uy * minU + vy * maxV)};
155+
}
156+
}
157+
158+
return new Rectangle(bestCorners, bestWidth, bestHeight, minArea);
159+
}
160+
161+
// -------------------- Helper Methods --------------------
162+
private static void orderCounterClockwise(List<Point> points) {
163+
double area = 0.0;
164+
for (int i = 0; i < points.size(); i++) {
165+
Point a = points.get(i);
166+
Point b = points.get((i + 1) % points.size());
167+
area += (a.x() * b.y()) - (b.x() * a.y());
168+
}
169+
if (area < 0) Collections.reverse(points);
170+
}
171+
172+
private static double distanceSquared(Point a, Point b) {
173+
double dx = a.x() - b.x();
174+
double dy = a.y() - b.y();
175+
return dx * dx + dy * dy;
176+
}
177+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package com.thealgorithms.geometry;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import java.util.Arrays;
6+
import java.util.List;
7+
import org.junit.jupiter.api.Test;
8+
9+
public class RotatingCalipersTest {
10+
11+
@Test
12+
void testDiameterSimpleTriangle() {
13+
List<Point> convexHull = Arrays.asList(new Point(0, 0), new Point(4, 0), new Point(2, 3));
14+
RotatingCalipers.PointPair result = RotatingCalipers.diameter(convexHull);
15+
16+
assertNotNull(result);
17+
assertEquals(4.0, result.distance(), 0.001);
18+
}
19+
20+
@Test
21+
void testDiameterSquare() {
22+
List<Point> convexHull = Arrays.asList(new Point(0, 0), new Point(3, 0), new Point(3, 3), new Point(0, 3));
23+
RotatingCalipers.PointPair result = RotatingCalipers.diameter(convexHull);
24+
25+
assertNotNull(result);
26+
assertEquals(Math.sqrt(18), result.distance(), 0.001);
27+
}
28+
29+
@Test
30+
void testWidthSimpleTriangle() {
31+
List<Point> convexHull = Arrays.asList(new Point(0, 0), new Point(4, 0), new Point(2, 3));
32+
double result = RotatingCalipers.width(convexHull);
33+
34+
// Updated expected width based on correct projection
35+
assertEquals(3, result, 0.1);
36+
}
37+
38+
@Test
39+
void testMinAreaBoundingRectangleSquare() {
40+
List<Point> convexHull = Arrays.asList(new Point(0, 0), new Point(3, 0), new Point(3, 3), new Point(0, 3));
41+
RotatingCalipers.Rectangle result = RotatingCalipers.minAreaBoundingRectangle(convexHull);
42+
43+
assertNotNull(result);
44+
assertEquals(9.0, result.area(), 0.1);
45+
assertEquals(3.0, result.width(), 0.1);
46+
assertEquals(3.0, result.height(), 0.1);
47+
48+
// Check corners are PointD and not null
49+
for (RotatingCalipers.PointD corner : result.corners()) {
50+
assertNotNull(corner);
51+
}
52+
}
53+
54+
@Test
55+
void testMinAreaBoundingRectangleTriangle() {
56+
List<Point> convexHull = Arrays.asList(new Point(0, 0), new Point(4, 0), new Point(2, 3));
57+
RotatingCalipers.Rectangle result = RotatingCalipers.minAreaBoundingRectangle(convexHull);
58+
59+
assertNotNull(result);
60+
assertEquals(4, result.corners().length);
61+
}
62+
63+
@Test
64+
void testDiameterWithLargeConvexHull() {
65+
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));
66+
RotatingCalipers.PointPair result = RotatingCalipers.diameter(convexHull);
67+
68+
assertNotNull(result);
69+
}
70+
71+
@Test
72+
void testWidthWithLargeConvexHull() {
73+
List<Point> convexHull = Arrays.asList(new Point(0, 0), new Point(3, 0), new Point(3, 3), new Point(0, 3));
74+
double result = RotatingCalipers.width(convexHull);
75+
76+
assertEquals(3.0, result, 0.001);
77+
}
78+
79+
@Test
80+
void testMinAreaBoundingRectangleWithLargeConvexHull() {
81+
List<Point> convexHull = Arrays.asList(new Point(0, 0), new Point(10, 0), new Point(10, 5), new Point(0, 5));
82+
RotatingCalipers.Rectangle result = RotatingCalipers.minAreaBoundingRectangle(convexHull);
83+
84+
assertNotNull(result);
85+
assertEquals(50.0, result.area(), 0.1);
86+
}
87+
}

0 commit comments

Comments
 (0)