Skip to content

Commit

Permalink
Merge pull request #147 from triya12/Rework-line-calc
Browse files Browse the repository at this point in the history
Rework line calculator
  • Loading branch information
creme332 authored Jul 7, 2024
2 parents 869e38c + 4d2e5c8 commit fba81b3
Show file tree
Hide file tree
Showing 2 changed files with 223 additions and 73 deletions.
115 changes: 95 additions & 20 deletions src/main/java/com/github/creme332/algorithms/LineCalculator.java
Original file line number Diff line number Diff line change
@@ -1,33 +1,63 @@
package com.github.creme332.algorithms;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;

public class LineCalculator {
private LineCalculator() {

}

/**
* Calculates pixels between any 2 points (x0, y0) and (x1, y1) using the DDA
* line algorithm. High precision calculations are used to reduce floating point
* errors.
*
* @param x0 x-coordinate of start point
* @param y0 y-coordinate of start point
* @param x1 x-coordinate of end point
* @param y1 y-coordinate of end point
* @return A 2D array with 2 elements. The first element is the array of
* x-coordinates and the second element is the array of y-coordinates.
*/
public static int[][] dda(int x0, int y0, int x1, int y1) {
int dx = x1 - x0;
int dy = y1 - y0;
int steps = Math.max(Math.abs(dx), Math.abs(dy));
final int dx = x1 - x0;
final int dy = y1 - y0;
final int steps = Math.max(Math.abs(dx), Math.abs(dy));

float xInc = (float) dx / steps;
float yInc = (float) dy / steps;
final BigDecimal xInc = BigDecimal.valueOf(dx).divide(BigDecimal.valueOf(steps), MathContext.DECIMAL128);
final BigDecimal yInc = BigDecimal.valueOf(dy).divide(BigDecimal.valueOf(steps), MathContext.DECIMAL128);

float x = x0;
float y = y0;
BigDecimal x = BigDecimal.valueOf(x0);
BigDecimal y = BigDecimal.valueOf(y0);

int[][] pixelCoords = new int[steps + 1][2];
int[] xpoints = new int[steps + 1];
int[] ypoints = new int[steps + 1];

for (int i = 0; i <= steps; i++) {
pixelCoords[i][0] = Math.round(x);
pixelCoords[i][1] = Math.round(y);
x += xInc;
y += yInc;
xpoints[i] = x.setScale(0, RoundingMode.HALF_UP).intValue();
ypoints[i] = y.setScale(0, RoundingMode.HALF_UP).intValue();

x = x.add(xInc);
y = y.add(yInc);
}

return pixelCoords;
return new int[][] { xpoints, ypoints };
}

/**
* Calculates pixels between any 2 points (x0, y0) and (x1, y1) using the
* Bresenham line algorithm.
*
* @param x0 x-coordinate of start point
* @param y0 y-coordinate of tart point
* @param x1 x-coordinate of end point
* @param y1 y-coordinate of end point
* @return A 2D array with 2 elements. The first element is the array of
* x-coordinates and the second element is the array of y-coordinates.
*/
public static int[][] bresenham(int x0, int y0, int x1, int y1) {
int dx = Math.abs(x1 - x0);
int dy = Math.abs(y1 - y0);
Expand All @@ -37,13 +67,12 @@ public static int[][] bresenham(int x0, int y0, int x1, int y1) {

int err = dx - dy;

int[][] pixelCoords = new int[dx + dy + 1][2];
int index = 0;
List<Integer> xpoints = new ArrayList<>();
List<Integer> ypoints = new ArrayList<>();

while (true) {
pixelCoords[index][0] = x0;
pixelCoords[index][1] = y0;
index++;
xpoints.add(x0);
ypoints.add(y0);

if (x0 == x1 && y0 == y1)
break;
Expand All @@ -59,6 +88,52 @@ public static int[][] bresenham(int x0, int y0, int x1, int y1) {
}
}

return pixelCoords;
return new int[][] { xpoints.stream().mapToInt(i -> i).toArray(), ypoints.stream().mapToInt(i -> i).toArray() };
}

/**
* Another version of the bresenham line algorithm.
* Adapted from: https://github.com/madbence/node-bresenham
*
* @param x0
* @param y0
* @param x1
* @param y1
* @return
*/
public static int[][] bresenham2(int x0, int y0, int x1, int y1) {
List<Integer> xpoints = new ArrayList<>();
List<Integer> ypoints = new ArrayList<>();

int dx = x1 - x0;
int dy = y1 - y0;
int adx = Math.abs(dx);
int ady = Math.abs(dy);
int sx = dx > 0 ? 1 : -1;
int sy = dy > 0 ? 1 : -1;
int eps = 0;
if (adx > ady) {
for (int x = x0, y = y0; sx < 0 ? x >= x1 : x <= x1; x += sx) {
xpoints.add(x);
ypoints.add(y);

eps += ady;
if (eps << 1 >= adx) {
y += sy;
eps -= adx;
}
}
} else {
for (int x = x0, y = y0; sy < 0 ? y >= y1 : y <= y1; y += sy) {
xpoints.add(x);
ypoints.add(y);
eps += adx;
if (eps << 1 >= ady) {
x += sx;
eps -= ady;
}
}
}
return new int[][] { xpoints.stream().mapToInt(i -> i).toArray(), ypoints.stream().mapToInt(i -> i).toArray() };
}
}
Original file line number Diff line number Diff line change
@@ -1,76 +1,151 @@
package com.github.creme332.tests.algorithms;

import org.junit.Ignore;
import static org.junit.Assert.assertArrayEquals;

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

import org.junit.Test;

import com.github.creme332.algorithms.LineCalculator;
import com.github.creme332.tests.utils.TestHelper;

public class LineCalculatorTest {
// Helper class to hold test cases
private static class TestCase {
private String description;
private int x0;
private int y0;
private int x1;
private int y1;
private int[][] expected;

@Ignore("Failing test to be fixed later")
public void testDrawDDA() {
int x0 = 2, y0 = 3, x1 = 10, y1 = 8;
int[][] expected = {
{ 2, 3 }, { 3, 3 }, { 4, 4 }, { 5, 4 }, { 6, 5 }, { 7, 5 }, { 8, 6 }, { 9, 7 }, { 10, 8 }
};

int[][] result = LineCalculator.dda(x0, y0, x1, y1);
TestHelper.assert2DArrayEquals(expected, result);
TestCase(String description, int x0, int y0, int x1, int y1, int[][] expected) {
this.description = description;
this.x0 = x0;
this.y0 = y0;
this.x1 = x1;
this.y1 = y1;
this.expected = expected;
}
}

@Ignore("Failing test to be fixed later")
public void testDrawBresenham() {
int x0 = 2, y0 = 3, x1 = 10, y1 = 8;
int[][] expected = {
{ 2, 3 }, { 3, 3 }, { 4, 4 }, { 5, 4 }, { 6, 5 }, { 7, 5 }, { 8, 6 }, { 9, 7 }, { 10, 8 }
};
public static Collection<TestCase> fixedTestCases() {
List<TestCase> testCases = new ArrayList<>();

testCases.add(new TestCase("m = 1", 1, 1, 5, 5, new int[][] {
{ 1, 2, 3, 4, 5 },
{ 1, 2, 3, 4, 5 }
}));

testCases.add(new TestCase("m = -1", -5, 5, -1, 1, new int[][] {
{ -5, -4, -3, -2, -1 },
{ 5, 4, 3, 2, 1 }
}));

testCases.add(new TestCase("0 < m < 1", 2, 1, 8, 5, new int[][] {
{ 2, 3, 4, 5, 6, 7, 8 },
{ 1, 2, 2, 3, 4, 4, 5 }
}));

testCases.add(new TestCase("-1 < m < 0", -6, 5, -1, 1, new int[][] {
{ -6, -5, -4, -3, -2, -1 },
{ 5, 4, 3, 3, 2, 1 }
}));

testCases.add(new TestCase("m > 1", 3, 2, 7, 8, new int[][] {
{ 3, 4, 4, 5, 6, 6, 7 },
{ 2, 3, 4, 5, 6, 7, 8 }
}));

testCases.add(new TestCase("m < -1", 2, 8, 5, 3, new int[][] {
{ 2, 3, 3, 4, 4, 5 },
{ 8, 7, 6, 5, 4, 3 }
}));

testCases.add(new TestCase("m < 0 in 1st quadrant", 8, 4, 6, 8, new int[][] {
{ 8, 8, 7, 7, 6 },
{ 4, 5, 6, 7, 8 }
}));

testCases.add(new TestCase("m = 0", 1, 1, 5, 1, new int[][] {
{ 1, 2, 3, 4, 5 },
{ 1, 1, 1, 1, 1 }
}));

int[][] result = LineCalculator.bresenham(x0, y0, x1, y1);
TestHelper.assert2DArrayEquals(expected, result);
testCases.add(new TestCase("m = INF", 0, 0, 0, 4, new int[][] {
{ 0, 0, 0, 0, 0 },
{ 0, 1, 2, 3, 4 }
}));

testCases.add(new TestCase("m has recurring decimal places", -10, -9, 7, 9, new int[][] {
{ -10, -9, -8, -7, -6, -5, -4, -3, -2, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7 },
{ -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }
}));
return testCases;
}

@Test
public void testDrawDDAHorizontal() {
int x0 = 1, y0 = 1, x1 = 5, y1 = 1;
int[][] expected = {
{ 1, 1 }, { 2, 1 }, { 3, 1 }, { 4, 1 }, { 5, 1 }
};

int[][] result = LineCalculator.dda(x0, y0, x1, y1);
TestHelper.assert2DArrayEquals(expected, result);
public void testDDA() {
for (TestCase test : fixedTestCases()) {
int[][] result = LineCalculator.dda(test.x0, test.y0, test.x1, test.y1);

try {
assertArrayEquals(test.expected, result);
} catch (AssertionError e) {
System.out.println(test.description);
throw e;
}

}
}

@Test
public void testDrawBresenhamHorizontal() {
int x0 = 1, y0 = 1, x1 = 5, y1 = 1;
int[][] expected = {
{ 1, 1 }, { 2, 1 }, { 3, 1 }, { 4, 1 }, { 5, 1 }
};

int[][] result = LineCalculator.bresenham(x0, y0, x1, y1);
TestHelper.assert2DArrayEquals(expected, result);
public void testBresenham() {
for (TestCase test : fixedTestCases()) {
int[][] result = LineCalculator.bresenham(test.x0, test.y0, test.x1, test.y1);

try {
assertArrayEquals(test.expected, result);
} catch (AssertionError e) {
System.out.println(test.description);
throw e;
}
}
}

@Test
public void testDrawDDAVertical() {
int x0 = 1, y0 = 1, x1 = 1, y1 = 5;
int[][] expected = {
{ 1, 1 }, { 1, 2 }, { 1, 3 }, { 1, 4 }, { 1, 5 }
};

int[][] result = LineCalculator.dda(x0, y0, x1, y1);
TestHelper.assert2DArrayEquals(expected, result);
public static int[] generateRandomCoordinate() {
final int BOUND = 10;
Random random = new Random();
int x = random.nextInt(2 * BOUND + 1) - BOUND; // Random integer between -BOUND and +BOUND
int y = random.nextInt(2 * BOUND + 1) - BOUND; // Random integer between -BOUND and +BOUND
return new int[] { x, y };
}

@Test
public void testDrawBresenhamVertical() {
int x0 = 1, y0 = 1, x1 = 1, y1 = 5;
int[][] expected = {
{ 1, 1 }, { 1, 2 }, { 1, 3 }, { 1, 4 }, { 1, 5 }
};

int[][] result = LineCalculator.bresenham(x0, y0, x1, y1);
TestHelper.assert2DArrayEquals(expected, result);
public void testRandom() {
final int NUM_TESTS = 100;

for (int i = 0; i < NUM_TESTS; i++) {
int[] start = generateRandomCoordinate();
int[] end = generateRandomCoordinate();

int[][] bresenhamResult = LineCalculator.bresenham(start[0], start[1], end[0], end[1]);
int[][] ddaResult = LineCalculator.dda(start[0], start[1], end[0], end[1]);

try {
assertArrayEquals(bresenhamResult, ddaResult);
} catch (AssertionError e) {
System.out.println(
"Random test failed for coordinates: " + Arrays.toString(start) + " "
+ Arrays.toString(end));
System.out.println("DDA: " + Arrays.deepToString(ddaResult));
System.out.println("Bresenham: " + Arrays.deepToString(bresenhamResult));
System.out.println();

throw e; // Re-throw the assertion error to ensure the test fails
}
}
}
}

0 comments on commit fba81b3

Please sign in to comment.