Skip to content

feat: add midpoint ellipse algorithm #5870

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions src/main/java/com/thealgorithms/geometry/MidpointEllipse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package com.thealgorithms.geometry;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
* The MidpointEllipse class implements the Midpoint Ellipse Drawing Algorithm.
* This algorithm efficiently computes the points on an ellipse by dividing it into two regions
* and using decision parameters to determine the next point to plot.
*/
public final class MidpointEllipse {

private MidpointEllipse() {
// Private constructor to prevent instantiation
}

/**
* Draws an ellipse using the Midpoint Ellipse Algorithm.
*
* @param centerX the x-coordinate of the center of the ellipse
* @param centerY the y-coordinate of the center of the ellipse
* @param a the length of the semi-major axis (horizontal radius)
* @param b the length of the semi-minor axis (vertical radius)
* @return a list of points (represented as int arrays) that form the ellipse
*/
public static List<int[]> drawEllipse(int centerX, int centerY, int a, int b) {
List<int[]> points = new ArrayList<>();

// Handle degenerate cases with early returns
if (a == 0 && b == 0) {
points.add(new int[] {centerX, centerY}); // Only the center point
return points;
}

if (a == 0) {
// Semi-major axis is zero, create a vertical line
for (int y = centerY - b; y <= centerY + b; y++) {
points.add(new int[] {centerX, y});
}
return points; // Early return
}

if (b == 0) {
// Semi-minor axis is zero, create a horizontal line
for (int x = centerX - a; x <= centerX + a; x++) {
points.add(new int[] {x, centerY});
}
return points; // Early return
}

// Normal case: Non-degenerate ellipse
computeEllipsePoints(points, centerX, centerY, a, b);

return points; // Return all calculated points of the ellipse
}

/**
* Computes points of a non-degenerate ellipse using the Midpoint Ellipse Algorithm.
*
* @param points the list to which points will be added
* @param centerX the x-coordinate of the center of the ellipse
* @param centerY the y-coordinate of the center of the ellipse
* @param a the length of the semi-major axis (horizontal radius)
* @param b the length of the semi-minor axis (vertical radius)
*/
private static void computeEllipsePoints(Collection<int[]> points, int centerX, int centerY, int a, int b) {
int x = 0; // Initial x-coordinate
int y = b; // Initial y-coordinate

// Region 1: Initial decision parameter
double d1 = (b * b) - (a * a * b) + (0.25 * a * a); // Decision variable for region 1
double dx = 2.0 * b * b * x; // Change in x
double dy = 2.0 * a * a * y; // Change in y

// Region 1: When the slope is less than 1
while (dx < dy) {
addEllipsePoints(points, centerX, centerY, x, y);

// Update decision parameter and variables
if (d1 < 0) {
x++;
dx += (2 * b * b); // Update x change
d1 += dx + (b * b); // Update decision parameter
} else {
x++;
y--;
dx += (2 * b * b); // Update x change
dy -= (2 * a * a); // Update y change
d1 += dx - dy + (b * b); // Update decision parameter
}
}

// Region 2: Initial decision parameter for the second region
double d2 = b * b * (x + 0.5) * (x + 0.5) + a * a * (y - 1) * (y - 1) - a * a * b * b;

// Region 2: When the slope is greater than or equal to 1
while (y >= 0) {
addEllipsePoints(points, centerX, centerY, x, y);

// Update decision parameter and variables
if (d2 > 0) {
y--;
dy -= (2 * a * a); // Update y change
d2 += (a * a) - dy; // Update decision parameter
} else {
y--;
x++;
dx += (2 * b * b); // Update x change
dy -= (2 * a * a); // Update y change
d2 += dx - dy + (a * a); // Update decision parameter
}
}
}

/**
* Adds points for all four quadrants of the ellipse based on symmetry.
*
* @param points the list to which points will be added
* @param centerX the x-coordinate of the center of the ellipse
* @param centerY the y-coordinate of the center of the ellipse
* @param x the x-coordinate relative to the center
* @param y the y-coordinate relative to the center
*/
private static void addEllipsePoints(Collection<int[]> points, int centerX, int centerY, int x, int y) {
points.add(new int[] {centerX + x, centerY + y});
points.add(new int[] {centerX - x, centerY + y});
points.add(new int[] {centerX + x, centerY - y});
points.add(new int[] {centerX - x, centerY - y});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.thealgorithms.geometry;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

/**
* The {@code MidpointEllipseTest} class contains unit tests for the
* {@code MidpointEllipse} class, specifically testing the
* {@code drawEllipse} method.
*
* <p>This class uses parameterized tests to validate the output of
* Midpoint Ellipse algorithm for various input points.</p>
*/
class MidpointEllipseTest {

/**
* Provides test cases for the drawEllipse method.
* Each argument contains: centerX, centerY, a, b, and expected points.
*
* @return a stream of arguments for parameterized testing
*/
static Stream<Arguments> ellipseTestProvider() {
return Stream.of(
Arguments.of(0, 0, 5, 3, new int[][] {{0, 3}, {0, 3}, {0, -3}, {0, -3}, {1, 3}, {-1, 3}, {1, -3}, {-1, -3}, {2, 3}, {-2, 3}, {2, -3}, {-2, -3}, {3, 2}, {-3, 2}, {3, -2}, {-3, -2}, {4, 2}, {-4, 2}, {4, -2}, {-4, -2}, {5, 1}, {-5, 1}, {5, -1}, {-5, -1}, {5, 0}, {-5, 0}, {5, 0}, {-5, 0}}),
Arguments.of(0, 0, 0, 5,
new int[][] {
{0, -5}, {0, -4}, {0, -3}, {0, -2}, {0, -1}, {0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5} // Only vertical line points and center
}),
Arguments.of(0, 0, 5, 0,
new int[][] {
{-5, 0}, {-4, 0}, {-3, 0}, {-2, 0}, {-1, 0}, {0, 0}, {1, 0}, {2, 0}, {3, 0}, {4, 0}, {5, 0} // Only horizontal line points and center
}),
Arguments.of(0, 0, 0, 0,
new int[][] {
{0, 0} // Only center point
}),
Arguments.of(0, 0, 4, 4,
new int[][] {
{0, 4},
{0, 4},
{0, -4},
{0, -4},
{1, 4},
{-1, 4},
{1, -4},
{-1, -4},
{2, 3},
{-2, 3},
{2, -3},
{-2, -3},
{3, 3},
{-3, 3},
{3, -3},
{-3, -3},
{3, 2},
{-3, 2},
{3, -2},
{-3, -2},
{4, 1},
{-4, 1},
{4, -1},
{-4, -1},
{4, 0},
{-4, 0},
{4, 0},
{-4, 0},
}));
}

/**
* Tests the drawEllipse method with various parameters.
*
* @param centerX the x-coordinate of the center of the ellipse
* @param centerY the y-coordinate of the center of the ellipse
* @param a the length of the semi-major axis
* @param b the length of the semi-minor axis
* @param expectedPoints the expected points forming the ellipse
*/
@ParameterizedTest
@MethodSource("ellipseTestProvider")
@DisplayName("Test drawing ellipses with various parameters")
void testDrawEllipse(int centerX, int centerY, int a, int b, int[][] expectedPoints) {
List<int[]> points = MidpointEllipse.drawEllipse(centerX, centerY, a, b);

// Validate the number of points and the specific points
assertEquals(expectedPoints.length, points.size(), "Number of points should match expected.");

for (int i = 0; i < expectedPoints.length; i++) {
assertArrayEquals(expectedPoints[i], points.get(i), "Point mismatch at index " + i);
}
}
}