This is a follow up of this post.
Changes:
- I tried harder to make my code readable yet concise.
- All
Point
instances are immutable and unique. - New, useful features have been added (like reflection, rotation, translation, etc.)
This class depends upon this class. Line
will follow soon. For the time being assume that Out.format(double)
uses String.format()
internally to make the output organised.
package library.geometry;
import com.sun.istack.internal.NotNull;
import library.Out;
import library.util.Numbers;
import java.util.Arrays;
/**
* This class is an abstraction of a point in the 2D, X-Y plane. A point has
* an abscissa (the X-coordinate) and an ordinate (the Y-coordinate) in the
* cartesian system of representation. In the polar representation, it has a
* radial distance (from the origin) and an angle (with the positive
* direction of the X-axis).
* <p>
* In order to avoid ambiguity, the constructor has been made private. Instead,
* convenient factory methods are provided in the {@code Factory} inner class.
* <p>
* <b>NOTE:</b> All instances of this class are immutable. All methods return
* new instances with (usually) different co-ordinates. Remember to store the
* result in a new variable, don't just invoke a method. <i>There are no
* {@code void} methods in this {@code class}.</i>
*
* @author Subhomoy Haldar (ambigram_maker)
* @version 2.0
*/
public class Point {
/**
* The abscissa or X-coordinate.
*/
private final double x;
/**
* The ordinate or Y-coordinate.
*/
private final double y;
/**
* This inner class provides the interface for creating new instances of
* the class {@code Point}.
*/
public static final class Factory {
/**
* Returns a point with the given cartesian co-ordinates.
*
* @param x The abscissa.
* @param y The ordinate.
* @return A point with the given cartesian co-ordinates.
*/
public static Point cartesianForm(double x, double y) {
return new Point(x, y);
}
/**
* Returns a point with the given polar co-ordinates.
*
* @param r The distance from the origin.
* @param theta The angle (in radians) made with the positive X-axis.
* @return A point with the given polar co-ordinates.
*/
public static Point polarForm(double r, double theta) {
return new Point(r * Math.cos(theta), r * Math.sin(theta));
}
}
/**
* The canonical constructor that takes cartesian co-ordinates.
*
* @param x The abscissa.
* @param y The ordinate.
*/
private Point(double x, double y) {
this.x = x;
this.y = y;
}
/**
* Returns the distance between the invoking point and the point passed as
* the argument.
*
* @param other The other point to measure the distance from.
* @return The distance between the invoking point and the point passed as
* the argument.
*/
public double getDistanceFrom(@NotNull Point other) {
double deltaX = this.x - other.x;
double deltaY = this.y - other.y;
return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
}
/**
* Returns the abscissa of the point.
*
* @return The abscissa of the point.
*/
public double getX() {
return x;
}
/**
* Returns the ordinate of the point.
*
* @return The ordinate of the point.
*/
public double getY() {
return y;
}
/**
* Returns the radial distance of the point (i.e distance from the origin).
*
* @return The radial distance of the point (i.e distance from the origin).
*/
public double getR() {
return Math.sqrt(x * x + y * y);
}
/**
* Returns the angle (in radians) made by the line joining this point
* and the origin with the positive direction of the X-axis.
*
* @return The angle (in radians) made by the line joining this point
* and the origin with the positive direction of the X-axis.
*/
public double getTheta() {
return Math.atan2(y, x);
}
/**
* Returns a new instance of {@code Point} that is the result of
* <i>translating</i> the current point by a specified amount in the
* positive direction of the axes.
*
* @param dx The distance which the point moves along the positive X-axis.
* @param dy The distance which the point moves along the positive Y-axis.
* @return A new instance of {@code Point} that is the result of
* <i>translating</i> the current point by a specified amount in the
* positive direction of the axes.
*/
@NotNull
public Point translate(double dx, double dy) {
return new Point(x + dx, y + dy);
}
/**
* Returns a new {@code Point} that would represent this point if the
* co-ordinate axes are rotated (in the anti-clockwise sense) by the
* specified angle (in radians).
*
* @param theta The angle (in radians) to rotate the axes by.
* @return A new {@code Point} that would represent this point if the
* co-ordinate axes are rotated (in the anti-clockwise sense) by the
* specified angle (in radians).
*/
@NotNull
public Point rotateAxesBy(double theta) {
double sin = Math.sin(theta);
double cos = Math.cos(theta);
double X = y * sin + x * cos;
double Y = y * cos - x * sin;
return new Point(X, Y);
}
/**
* Returns a new instance of {@code Point} that has the same co-ordinates
* as {@code this} one.
*
* @return A new instance of {@code Point} that has the same co-ordinates
* as {@code this} one.
*/
@NotNull
public Point copy() {
return new Point(x, y);
}
/**
* Returns the image of this {@code Point} that is obtained by reflection in
* the given {@code Line}.
*
* @param line The <i>surface</i> for reflection.
* @return The image of this {@code Point} that is obtained by reflection in
* the given {@code Line}.
*/
public Point getReflectionFrom(Line line) {
Line perpendicular = line.getPerpendicularAt(this);
Point intersection = line.getIntersectionPointWith(perpendicular);
double deltaX = intersection.x - this.x;
double deltaY = intersection.y - this.y;
return intersection.translate(deltaX, deltaY);
}
/**
* Returns {@code true} if the invoking instance and the argument
* represent the same {@code Point}.
* <p>
* Due to the use of the floating-point data-type - {@code double}, there
* are some inherent rounding errors. Therefore, the co-ordinates are
* compared within a tolerance limit to ensure proper functioning.
*
* @param other The other {@code Object} to compare with.
* @return {@code true} if the invoking instance and the argument
* represent the same {@code Point}.
*/
@Override
public boolean equals(Object other) {
if (other instanceof Point) {
Point term = (Point) other;
return this == term ||
Numbers.areEqual(this.x, term.x) &&
Numbers.areEqual(this.y, term.y);
}
return false;
}
/**
* Returns the hash code for {@code this Point}.
*
* @return The hash code for {@code this Point}.
*/
@Override
public int hashCode() {
return Arrays.hashCode(new double[]{x, y, super.hashCode()});
}
/**
* Returns the String representation of {@code this Point}.
*
* @return The String representation of {@code this Point}.
*/
@Override
public String toString() {
return "(" + Out.format(x) + "," + Out.format(y) + ")";
}
}
The Points
is for Point
as Paths
is for Path
:
package library.geometry;
import com.sun.istack.internal.NotNull;
/**
* This class has all the static methods that are necessary for creation and
* manipulation of instances of the class {@code Point}.
*
* @author Subhomoy Haldar
* @version 1.0
*/
public class Points {
/**
* This static variable defines the origin (i.e. the point (0, 0)).
*/
public static final Point ORIGIN = Point.Factory.cartesianForm(0, 0);
/**
* Returns {@code true} if the {@code Point}s passed as arguments are
* collinear. This means that there is a single line possible that
* passes through all the {@code Point}s.
* <p>
*
* @param points The {@code Point}s to check for collinearity.
* @return {@code true} if the {@code Point}s passed as arguments are
* collinear.
*/
public static boolean areCollinear(@NotNull Point... points) {
int length = points.length;
if (length < 3) {
throw new IllegalArgumentException("There must at least 3 Points.");
}
Line line = Line.Factory.twoPointForm(points[0], points[1]);
for (int i = 2; i < length; i++) {
if (!line.passesThrough(points[i])) {
return false;
}
}
return true;
}
}
It doesn't boast much. It will evolve as time goes.
The tests are written rather hastily:
package library.geometry;
import org.junit.Test;
import library.util.Numbers;
import static org.junit.Assert.*;
/**
* @author Subhomoy Haldar
* @version 1.0
*/
public class PointTest {
@Test
public void testGetCartesian() throws Exception {
Point point1 = Point.Factory.cartesianForm(3, 4);
Point point2 = Point.Factory.polarForm(5, Math.atan2(4.0, 3.0));
assertEquals(point1, point2);
}
@Test
public void testEquality1() throws Exception {
Point point1 = Points.ORIGIN;
Point point2 = Point.Factory.cartesianForm(2, 2).translate(-2, -2);
assertEquals(point1, point2);
}
@Test
public void testDistanceFrom() throws Exception {
Point origin = Points.ORIGIN;
Point point1 = Point.Factory.cartesianForm(3, 4);
double distance = origin.getDistanceFrom(point1);
assertTrue(Numbers.areEqual(distance, 5));
}
@Test
public void testRotation() throws Exception {
Point point1 = Point.Factory.polarForm(5, Math.toRadians(30));
Point point2 = Point.Factory.polarForm(5, Math.toRadians(60));
assertEquals(point2.rotateAxesBy(Math.toRadians(30)), point1);
}
@Test
public void testCollinear() throws Exception {
Point point1 = Point.Factory.cartesianForm(0, 0);
Point point2 = Point.Factory.cartesianForm(3, 4);
Point point3 = Point.Factory.polarForm(5, Math.atan2(4, 3));
assertTrue(Points.areCollinear(point1, point2, point3));
}
@Test
public void testReflection() throws Exception {
Point point = Point.Factory.cartesianForm(4, 4);
Line line1 = Line.Factory.generalForm(1, 1, 0);
Line line2 = Line.Factory.generalForm(1, 0, -2);
assertTrue(point.getReflectionFrom(line1)
.equals(Point.Factory.cartesianForm(-4, -4)));
assertTrue(point.getReflectionFrom(line2)
.equals(Point.Factory.cartesianForm(0, 4)));
}
}
I uploaded this class for (complete) review as well as to explain the use case of Numbers
. Hopefully, the code is better than what I wrote last time. More examples for test cases will be appreciated.