This is an automated email from the ASF dual-hosted git repository. erans pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-geometry.git
The following commit(s) were added to refs/heads/master by this push: new a26b977 GEOMETRY-28: Cleaning up o.a.c.geometry.euclidean.twod.Line API and adding additional unit tests new a05c6ab Merge branch 'GEOMETRY-28__matt' a26b977 is described below commit a26b977a02ca271c89c6b16f0ef1742bdde36755 Author: Matt Juntunen <matt.juntu...@hotmail.com> AuthorDate: Sat Feb 16 21:41:22 2019 -0500 GEOMETRY-28: Cleaning up o.a.c.geometry.euclidean.twod.Line API and adding additional unit tests --- .../geometry/euclidean/oned/OrientedPoint.java | 2 +- .../euclidean/threed/OutlineExtractor.java | 6 +- .../geometry/euclidean/threed/PolyhedronsSet.java | 19 +- .../geometry/euclidean/threed/SubPlane.java | 4 +- .../euclidean/twod/AffineTransformMatrix2D.java | 27 + .../commons/geometry/euclidean/twod/Line.java | 597 ++++++------- .../geometry/euclidean/twod/NestedLoops.java | 2 +- .../geometry/euclidean/twod/PolygonsSet.java | 16 +- .../commons/geometry/euclidean/twod/SubLine.java | 4 +- .../core/partitioning/CharacterizationTest.java | 4 +- .../geometry/euclidean/EuclideanTestUtils.java | 2 +- .../twod/AffineTransformMatrix2DTest.java | 33 + .../commons/geometry/euclidean/twod/LineTest.java | 993 +++++++++++++++++++-- .../geometry/euclidean/twod/PolygonsSetTest.java | 76 +- .../geometry/euclidean/twod/SegmentTest.java | 2 +- .../geometry/euclidean/twod/SubLineTest.java | 12 +- .../geometry/euclidean/twod/hull/ConvexHull2D.java | 6 +- .../euclidean/twod/hull/MonotoneChain.java | 2 +- .../euclidean/twod/hull/MonotoneChainTest.java | 2 +- 19 files changed, 1360 insertions(+), 449 deletions(-) diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java index 4b01e66..23ad001 100644 --- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java +++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java @@ -172,7 +172,7 @@ public final class OrientedPoint implements Hyperplane<Vector1D>, Serializable { int result = 1; result = (prime * result) + Objects.hashCode(location); result = (prime * result) + Boolean.hashCode(positiveFacing); - result = (prime * result) + Objects.hash(precision); + result = (prime * result) + Objects.hashCode(precision); return result; } diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/OutlineExtractor.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/OutlineExtractor.java index c1a4eb2..48c8181 100644 --- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/OutlineExtractor.java +++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/OutlineExtractor.java @@ -214,7 +214,7 @@ public class OutlineExtractor { final Vector2D cPoint = Vector2D.of(current3D.dot(u), current3D.dot(v)); final org.apache.commons.geometry.euclidean.twod.Line line = - new org.apache.commons.geometry.euclidean.twod.Line(pPoint, cPoint, precision); + org.apache.commons.geometry.euclidean.twod.Line.fromPoints(pPoint, cPoint, precision); SubHyperplane<Vector2D> edge = line.wholeHyperplane(); if (closed || (previous != 1)) { @@ -222,7 +222,7 @@ public class OutlineExtractor { // it defines one bounding point of the edge final double angle = line.getAngle() + 0.5 * Math.PI; final org.apache.commons.geometry.euclidean.twod.Line l = - new org.apache.commons.geometry.euclidean.twod.Line(pPoint, angle, precision); + org.apache.commons.geometry.euclidean.twod.Line.fromPointAndAngle(pPoint, angle, precision); edge = edge.split(l).getPlus(); } @@ -231,7 +231,7 @@ public class OutlineExtractor { // it defines one bounding point of the edge final double angle = line.getAngle() + 0.5 * Math.PI; final org.apache.commons.geometry.euclidean.twod.Line l = - new org.apache.commons.geometry.euclidean.twod.Line(cPoint, angle, precision); + org.apache.commons.geometry.euclidean.twod.Line.fromPointAndAngle(cPoint, angle, precision); edge = edge.split(l).getMinus(); } diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PolyhedronsSet.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PolyhedronsSet.java index a30180c..d522f7c 100644 --- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PolyhedronsSet.java +++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PolyhedronsSet.java @@ -628,13 +628,10 @@ public class PolyhedronsSet extends AbstractRegion<Vector3D, Vector2D> { cachedOriginal = (Plane) original; cachedTransform = - org.apache.commons.geometry.euclidean.twod.Line.getTransform(tP10.getX() - tP00.getX(), - tP10.getY() - tP00.getY(), - tP01.getX() - tP00.getX(), - tP01.getY() - tP00.getY(), - tP00.getX(), - tP00.getY()); - + org.apache.commons.geometry.euclidean.twod.Line.getTransform( + tP00.vectorTo(tP10), + tP00.vectorTo(tP01), + tP00); } return ((SubLine) sub).applyTransform(cachedTransform); } @@ -695,10 +692,10 @@ public class PolyhedronsSet extends AbstractRegion<Vector3D, Vector2D> { cachedOriginal = (Plane) original; cachedTransform = - org.apache.commons.geometry.euclidean.twod.Line.getTransform(1, 0, 0, 1, - shift.getX(), - shift.getY()); - + org.apache.commons.geometry.euclidean.twod.Line.getTransform( + Vector2D.PLUS_X, + Vector2D.PLUS_Y, + shift); } return ((SubLine) sub).applyTransform(cachedTransform); diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubPlane.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubPlane.java index da6d154..02f646e 100644 --- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubPlane.java +++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubPlane.java @@ -84,9 +84,9 @@ public class SubPlane extends AbstractSubHyperplane<Vector3D, Vector2D> { q = tmp; } final SubHyperplane<Vector2D> l2DMinus = - new org.apache.commons.geometry.euclidean.twod.Line(p, q, precision).wholeHyperplane(); + org.apache.commons.geometry.euclidean.twod.Line.fromPoints(p, q, precision).wholeHyperplane(); final SubHyperplane<Vector2D> l2DPlus = - new org.apache.commons.geometry.euclidean.twod.Line(q, p, precision).wholeHyperplane(); + org.apache.commons.geometry.euclidean.twod.Line.fromPoints(q, p, precision).wholeHyperplane(); final BSPTree<Vector2D> splitTree = getRemainingRegion().getTree(false).split(l2DMinus); final BSPTree<Vector2D> plusTree = getRemainingRegion().isEmpty(splitTree.getPlus()) ? diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2D.java index 0f7e416..2192f43 100644 --- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2D.java +++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2D.java @@ -414,6 +414,33 @@ public final class AffineTransformMatrix2D implements AffineTransformMatrix<Vect ); } + /** Get a new transform create from the given column vectors. The returned transform + * does not include any translation component. + * @param u first column vector; this corresponds to the first basis vector + * in the coordinate frame + * @param v second column vector; this corresponds to the second basis vector + * in the coordinate frame + * @return a new transform with the given column vectors + */ + public static AffineTransformMatrix2D fromColumnVectors(final Vector2D u, final Vector2D v) { + return fromColumnVectors(u, v, Vector2D.ZERO); + } + + /** Get a new transform created from the given column vectors. + * @param u first column vector; this corresponds to the first basis vector + * in the coordinate frame + * @param v second column vector; this corresponds to the second basis vector + * in the coordinate frame + * @param t third column vector; this corresponds to the translation of the transform + * @return a new transform with the given column vectors + */ + public static AffineTransformMatrix2D fromColumnVectors(final Vector2D u, final Vector2D v, final Vector2D t) { + return new AffineTransformMatrix2D( + u.getX(), v.getX(), t.getX(), + u.getY(), v.getY(), t.getY() + ); + } + /** Get the transform representing the identity matrix. This transform does not * modify point or vector values when applied. * @return transform representing the identity matrix diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java index 37d4a6c..2fc9446 100644 --- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java +++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java @@ -16,6 +16,10 @@ */ package org.apache.commons.geometry.euclidean.twod; +import java.io.Serializable; +import java.util.Objects; + +import org.apache.commons.geometry.core.exception.GeometryValueException; import org.apache.commons.geometry.core.partitioning.Embedding; import org.apache.commons.geometry.core.partitioning.Hyperplane; import org.apache.commons.geometry.core.partitioning.SubHyperplane; @@ -29,225 +33,168 @@ import org.apache.commons.numbers.arrays.LinearCombination; /** This class represents an oriented line in the 2D plane. - * <p>An oriented line can be defined either by prolongating a line - * segment between two points past these points, or by one point and - * an angular direction (in trigonometric orientation).</p> + * <p>An oriented line can be defined either by extending a line + * segment between two points past these points, by specifying a + * point and a direction, or by specifying a point and an angle + * relative to the x-axis.</p> - * <p>Since it is oriented the two half planes at its two sides are - * unambiguously identified as a left half plane and a right half + * <p>Since the line oriented, the two half planes on its sides are + * unambiguously identified as the left half plane and the right half * plane. This can be used to identify the interior and the exterior - * in a simple way by local properties only when part of a line is - * used to define part of a polygon boundary.</p> + * in a simple way when a line is used to define a portion of a polygon + * boundary.</p> * <p>A line can also be used to completely define a reference frame * in the plane. It is sufficient to select one specific point in the * line (the orthogonal projection of the original reference frame on - * the line) and to use the unit vector in the line direction and the - * orthogonal vector oriented from left half plane to right half - * plane. We define two coordinates by the process, the - * <em>abscissa</em> along the line, and the <em>offset</em> across - * the line. All points of the plane are uniquely identified by these - * two coordinates. The line is the set of points at zero offset, the - * left half plane is the set of points with negative offsets and the - * right half plane is the set of points with positive offsets.</p> + * the line) and to use the unit vector in the line direction (see + * {@link #getDirection()} and the orthogonal vector oriented from the + * left half plane to the right half plane (see {@link #getOffsetDirection()}. + * We define two coordinates by the process, the <em>abscissa</em> along + * the line, and the <em>offset</em> across the line. All points of the + * plane are uniquely identified by these two coordinates. The line is + * the set of points at zero offset, the left half plane is the set of + * points with negative offsets and the right half plane is the set of + * points with positive offsets.</p> */ -public class Line implements Hyperplane<Vector2D>, Embedding<Vector2D, Vector1D> { - /** Angle with respect to the abscissa axis. */ - private double angle; +public final class Line implements Hyperplane<Vector2D>, Embedding<Vector2D, Vector1D>, Serializable { - /** Cosine of the line angle. */ - private double cos; + /** Serializable UID. */ + private static final long serialVersionUID = 20190120L; - /** Sine of the line angle. */ - private double sin; + /** The direction of the line as a normalized vector. */ + private final Vector2D direction; - /** Offset of the frame origin. */ - private double originOffset; + /** The distance between the origin and the line. */ + private final double originOffset; /** Precision context used to compare floating point numbers. */ private final DoublePrecisionContext precision; - /** Reverse line. */ - private Line reverse; - - /** Build a line from two points. - * <p>The line is oriented from p1 to p2</p> - * @param p1 first point - * @param p2 second point - * @param precision precision context used to compare floating point values + /** Simple constructor. + * @param direction The direction of the line. + * @param originOffset The signed distance between the line and the origin. + * @param precision Precision context used to compare floating point numbers. */ - public Line(final Vector2D p1, final Vector2D p2, final DoublePrecisionContext precision) { - reset(p1, p2); + private Line(final Vector2D direction, final double originOffset, final DoublePrecisionContext precision) { + this.direction = direction; + this.originOffset = originOffset; this.precision = precision; } - /** Build a line from a point and an angle. - * @param p point belonging to the line - * @param angle angle of the line with respect to abscissa axis - * @param precision precision context used to compare floating point values + /** Get the angle of the line in radians with respect to the abscissa (+x) axis. The + * returned angle is in the range {@code [0, 2pi)}. + * @return the angle of the line with respect to the abscissa (+x) axis in the range + * {@code [0, 2pi)} */ - public Line(final Vector2D p, final double angle, final DoublePrecisionContext precision) { - reset(p, angle); - this.precision = precision; + public double getAngle() { + final double angle = Math.atan2(direction.getY(), direction.getX()); + return PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(angle); } - /** Build a line from its internal characteristics. - * @param angle angle of the line with respect to abscissa axis - * @param cos cosine of the angle - * @param sin sine of the angle - * @param originOffset offset of the origin - * @param precision precision context used to compare floating point values + /** Get the direction of the line. + * @return the direction of the line */ - private Line(final double angle, final double cos, final double sin, - final double originOffset, final DoublePrecisionContext precision) { - this.angle = angle; - this.cos = cos; - this.sin = sin; - this.originOffset = originOffset; - this.precision = precision; - this.reverse = null; + public Vector2D getDirection() { + return direction; } - /** Copy constructor. - * <p>The created instance is completely independent from the - * original instance, it is a deep copy.</p> - * @param line line to copy + /** Get the offset direction of the line. This vector is perpendicular to the + * line and points in the direction of positive offset values, meaning that + * it points from the left side of the line to the right when one is looking + * along the line direction. + * @return the offset direction of the line. */ - public Line(final Line line) { - angle = PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(line.angle); - cos = line.cos; - sin = line.sin; - originOffset = line.originOffset; - precision = line.precision; - reverse = null; + public Vector2D getOffsetDirection() { + return Vector2D.of(direction.getY(), -direction.getX()); } - /** {@inheritDoc} */ - @Override - public Line copySelf() { - return new Line(this); + /** Get the line origin point. This is the projection of the 2D origin + * onto the line and also serves as the origin for the 1D embedded subspace. + * @return the origin point of the line + */ + public Vector2D getOrigin() { + return toSpace(Vector1D.ZERO); } - /** Reset the instance as if built from two points. - * <p>The line is oriented from p1 to p2</p> - * @param p1 first point - * @param p2 second point + /** Get the signed distance from the origin of the 2D space to the + * closest point on the line. + * @return the signed distance from the origin to the line */ - public void reset(final Vector2D p1, final Vector2D p2) { - unlinkReverse(); - final double dx = p2.getX() - p1.getX(); - final double dy = p2.getY() - p1.getY(); - final double d = Math.hypot(dx, dy); - if (d == 0.0) { - angle = 0.0; - cos = 1.0; - sin = 0.0; - originOffset = p1.getY(); - } else { - angle = Math.PI + Math.atan2(-dy, -dx); - cos = dx / d; - sin = dy / d; - originOffset = LinearCombination.value(p2.getX(), p1.getY(), -p1.getX(), p2.getY()) / d; - } + public double getOriginOffset() { + return originOffset; } - /** Reset the instance as if built from a line and an angle. - * @param p point belonging to the line - * @param alpha angle of the line with respect to abscissa axis - */ - public void reset(final Vector2D p, final double alpha) { - unlinkReverse(); - this.angle = PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(alpha); - cos = Math.cos(this.angle); - sin = Math.sin(this.angle); - originOffset = LinearCombination.value(cos, p.getY(), -sin, p.getX()); + /** {@inheritDoc} */ + @Override + public DoublePrecisionContext getPrecision() { + return precision; } - /** Revert the instance. - */ - public void revertSelf() { - unlinkReverse(); - if (angle < Math.PI) { - angle += Math.PI; - } else { - angle -= Math.PI; - } - cos = -cos; - sin = -sin; - originOffset = -originOffset; + /** {@inheritDoc} */ + @Override + public Line copySelf() { + return this; } - /** Unset the link between an instance and its reverse. - */ - private void unlinkReverse() { - if (reverse != null) { - reverse.reverse = null; - } - reverse = null; - } - - /** Get the reverse of the instance. - * <p>Get a line with reversed orientation with respect to the - * instance.</p> - * <p> - * As long as neither the instance nor its reverse are modified - * (i.e. as long as none of the {@link #reset(Vector2D, Vector2D)}, - * {@link #reset(Vector2D, double)}, {@link #revertSelf()}, - * {@link #setAngle(double)} or {@link #setOriginOffset(double)} - * methods are called), then the line and its reverse remain linked - * together so that {@code line.getReverse().getReverse() == line}. - * When one of the line is modified, the link is deleted as both - * instance becomes independent. - * </p> + /** Get the reverse of the instance, meaning a line containing the same + * points but with the opposite orientation. * @return a new line, with orientation opposite to the instance orientation */ - public Line getReverse() { - if (reverse == null) { - reverse = new Line((angle < Math.PI) ? (angle + Math.PI) : (angle - Math.PI), - -cos, -sin, -originOffset, precision); - reverse.reverse = this; - } - return reverse; + public Line reverse() { + return new Line(direction.negate(), -originOffset, precision); } /** {@inheritDoc} */ @Override public Vector1D toSubSpace(final Vector2D point) { - return Vector1D.of(LinearCombination.value(cos, point.getX(), sin, point.getY())); + return Vector1D.of(direction.dot(point)); } /** {@inheritDoc} */ @Override public Vector2D toSpace(final Vector1D point) { final double abscissa = point.getX(); - return Vector2D.of(LinearCombination.value(abscissa, cos, -originOffset, sin), - LinearCombination.value(abscissa, sin, originOffset, cos)); + + // The 2D coordinate is equal to the projection of the + // 2D origin onto the line plus the direction multiplied + // by the abscissa. We can combine everything into a single + // step below given that the origin location is equal to + // (-direction.y * originOffset, direction.x * originOffset). + return Vector2D.of( + LinearCombination.value(abscissa, direction.getX(), -originOffset, direction.getY()), + LinearCombination.value(abscissa, direction.getY(), originOffset, direction.getX()) + ); } /** Get the intersection point of the instance and another line. * @param other other line * @return intersection point of the instance and the other line - * or null if there are no intersection points + * or null if there is no unique intersection point (ie, the lines + * are parallel or coincident) */ public Vector2D intersection(final Line other) { - final double d = LinearCombination.value(sin, other.cos, -other.sin, cos); - if (precision.eqZero(d)) { + final double area = this.direction.signedArea(other.direction); + if (precision.eqZero(area)) { + // lines are parallel return null; } - return Vector2D.of(LinearCombination.value(cos, other.originOffset, -other.cos, originOffset) / d, - LinearCombination.value(sin, other.originOffset, -other.sin, originOffset) / d); - } - /** {@inheritDoc} */ - @Override - public Vector2D project(Vector2D point) { - return toSpace(toSubSpace(point)); + final double x = LinearCombination.value( + other.direction.getX(), originOffset, + -direction.getX(), other.originOffset) / area; + + final double y = LinearCombination.value( + other.direction.getY(), originOffset, + -direction.getY(), other.originOffset) / area; + + return Vector2D.of(x, y); } /** {@inheritDoc} */ @Override - public DoublePrecisionContext getPrecision() { - return precision; + public Vector2D project(final Vector2D point) { + return toSpace(toSubSpace(point)); } /** {@inheritDoc} */ @@ -265,45 +212,61 @@ public class Line implements Hyperplane<Vector2D>, Embedding<Vector2D, Vector1D> return new PolygonsSet(precision); } - /** Get the offset (oriented distance) of a parallel line. - * <p>This method should be called only for parallel lines otherwise - * the result is not meaningful.</p> - * <p>The offset is 0 if both lines are the same, it is - * positive if the line is on the right side of the instance and - * negative if it is on the left side, according to its natural - * orientation.</p> + /** {@inheritDoc} */ + @Override + public double getOffset(final Vector2D point) { + return originOffset - direction.signedArea(point); + } + + /** Get the offset (oriented distance) of a line. Since an infinite + * number of distances can be calculated between points on two different + * lines, this methods returns the value closest to zero. For intersecting + * lines, this will simply be zero. For parallel lines, this will be the + * perpendicular distance between the two lines, as a signed value. + * + * <p>The sign of the returned offset indicates the side of the line that the + * argument lies on. The offset is positive if the line lies on the right side + * of the instance and negative if the line lies on the left side + * of the instance.</p> * @param line line to check * @return offset of the line + * @see #distance(Line) */ public double getOffset(final Line line) { - return originOffset + - (LinearCombination.value(cos, line.cos, sin, line.sin) > 0 ? -line.originOffset : line.originOffset); - } + if (isParallel(line)) { + // since the lines are parallel, the offset between + // them is simply the difference between their origin offsets, + // with the second offset negated if the lines point if opposite + // directions + final double dot = direction.dot(line.direction); + return originOffset - (Math.signum(dot) * line.originOffset); + } - /** {@inheritDoc} */ - @Override - public double getOffset(final Vector2D point) { - return LinearCombination.value(sin, point.getX(), -cos, point.getY(), 1.0, originOffset); + // the lines are not parallel, which means they intersect at some point + return 0.0; } /** {@inheritDoc} */ @Override public boolean sameOrientationAs(final Hyperplane<Vector2D> other) { - final Line otherL = (Line) other; - return LinearCombination.value(sin, otherL.sin, cos, otherL.cos) >= 0.0; + final Line otherLine = (Line) other; + return direction.dot(otherLine.direction) >= 0.0; } - /** Get one point from the plane. - * @param abscissa desired abscissa for the point - * @param offset desired offset for the point + /** Get one point from the plane, relative to the coordinate system + * of the line. Note that the direction of increasing offsets points + * to the <em>right</em> of the line. This means that if one pictures + * the line (abscissa) direction as equivalent to the +x-axis, the offset + * direction will point along the -y axis. + * @param abscissa desired abscissa (distance along the line) for the point + * @param offset desired offset (distance perpendicular to the line) for the point * @return one point in the plane, with given abscissa and offset - * relative to the line + * relative to the line */ - public Vector2D getPointAt(final Vector1D abscissa, final double offset) { - final double x = abscissa.getX(); - final double dOffset = offset - originOffset; - return Vector2D.of(LinearCombination.value(x, cos, dOffset, sin), - LinearCombination.value(x, sin, -dOffset, cos)); + public Vector2D pointAt(final double abscissa, final double offset) { + final double pointOffset = offset - originOffset; + return Vector2D.of(LinearCombination.value(abscissa, direction.getX(), pointOffset, direction.getY()), + LinearCombination.value(abscissa, direction.getY(), -pointOffset, direction.getX())); } /** Check if the line contains a point. @@ -314,6 +277,16 @@ public class Line implements Hyperplane<Vector2D>, Embedding<Vector2D, Vector1D> return precision.eqZero(getOffset(p)); } + /** Check if this instance completely contains the other line. + * This will be true if the two instances represent the same line, + * with perhaps different directions. + * @param line line to check + * @return true if this instance contains all points in the given line + */ + public boolean contains(final Line line) { + return isParallel(line) && precision.eqZero(getOffset(line)); + } + /** Compute the distance between the instance and a point. * <p>This is a shortcut for invoking Math.abs(getOffset(p)), * and provides consistency with what is in the @@ -326,164 +299,174 @@ public class Line implements Hyperplane<Vector2D>, Embedding<Vector2D, Vector1D> return Math.abs(getOffset(p)); } - /** Check the instance is parallel to another line. - * @param line other line to check - * @return true if the instance is parallel to the other line - * (they can have either the same or opposite orientations) + /** Compute the shortest distance between this instance and + * the given line. This value will simply be zero for intersecting + * lines. + * @param line line to compute the closest distance to + * @return the shortest distance between this instance and the + * given line + * @see #getOffset(Line) */ - public boolean isParallelTo(final Line line) { - return precision.eqZero(LinearCombination.value(sin, line.cos, -cos, line.sin)); + public double distance(final Line line) { + return Math.abs(getOffset(line)); } - /** Translate the line to force it passing by a point. - * @param p point by which the line should pass + /** Check if the instance is parallel to another line. + * @param line other line to check + * @return true if the instance is parallel to the other line + * (they can have either the same or opposite orientations) */ - public void translateToPoint(final Vector2D p) { - originOffset = LinearCombination.value(cos, p.getY(), -sin, p.getX()); + public boolean isParallel(final Line line) { + final double area = direction.signedArea(line.direction); + return precision.eqZero(area); } - /** Get the angle of the line. - * @return the angle of the line with respect to the abscissa axis + /** Transform this instance with the given transform. + * @param transform transform to apply to this instance + * @return a new transformed line */ - public double getAngle() { - return PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(angle); - } + public Line transform(final Transform<Vector2D, Vector1D> transform) { + final Vector2D origin = getOrigin(); - /** Set the angle of the line. - * @param angle new angle of the line with respect to the abscissa axis - */ - public void setAngle(final double angle) { - unlinkReverse(); - this.angle = PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(angle); - cos = Math.cos(this.angle); - sin = Math.sin(this.angle); - } + final Vector2D p1 = transform.apply(origin); + final Vector2D p2 = transform.apply(origin.add(direction)); - /** Get the offset of the origin. - * @return the offset of the origin - */ - public double getOriginOffset() { - return originOffset; + return Line.fromPoints(p1, p2, precision); } - /** Set the offset of the origin. - * @param offset offset of the origin - */ - public void setOriginOffset(final double offset) { - unlinkReverse(); - originOffset = offset; - } - - /** Get a {@link org.apache.commons.geometry.core.partitioning.Transform - * Transform} embedding an affine transform. - * @param cXX transform factor between input abscissa and output abscissa - * @param cYX transform factor between input abscissa and output ordinate - * @param cXY transform factor between input ordinate and output abscissa - * @param cYY transform factor between input ordinate and output ordinate - * @param cX1 transform addendum for output abscissa - * @param cY1 transform addendum for output ordinate - * @return a new transform that can be applied to either {@link - * Vector2D}, {@link Line Line} or {@link - * org.apache.commons.geometry.core.partitioning.SubHyperplane - * SubHyperplane} instances - * @exception IllegalArgumentException if the transform is non invertible - */ - public static Transform<Vector2D, Vector1D> getTransform(final double cXX, - final double cYX, - final double cXY, - final double cYY, - final double cX1, - final double cY1) - throws IllegalArgumentException { - return new LineTransform(cXX, cYX, cXY, cYY, cX1, cY1); - } - - /** Class embedding an affine transform. - * <p>This class is used in order to apply an affine transform to a - * line. Using a specific object allow to perform some computations - * on the transform only once even if the same transform is to be - * applied to a large number of lines (for example to a large - * polygon)./<p> - */ - private static class LineTransform implements Transform<Vector2D, Vector1D> { + /** {@inheritDoc} */ + @Override + public int hashCode() { + final int prime = 167; - /** Transform factor between input abscissa and output abscissa. */ - private final double cXX; + int result = 1; + result = (prime * result) + Objects.hashCode(direction); + result = (prime * result) + Double.hashCode(originOffset); + result = (prime * result) + Objects.hashCode(precision); - /** Transform factor between input abscissa and output ordinate. */ - private final double cYX; + return result; + } - /** Transform factor between input ordinate and output abscissa. */ - private final double cXY; + /** {@inheritDoc} */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + else if (!(obj instanceof Line)) { + return false; + } - /** Transform factor between input ordinate and output ordinate. */ - private final double cYY; + Line other = (Line) obj; - /** Transform addendum for output abscissa. */ - private final double cX1; + return Objects.equals(this.direction, other.direction) && + Double.compare(this.originOffset, other.originOffset) == 0 && + Objects.equals(this.precision, other.precision); + } - /** Transform addendum for output ordinate. */ - private final double cY1; + /** {@inheritDoc} */ + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()) + .append("[origin= ") + .append(getOrigin()) + .append(", direction= ") + .append(direction) + .append(']'); + + return sb.toString(); + } - /** cXY * cY1 - cYY * cX1. */ - private final double c1Y; + /** Create a line from two points lying on the line. The line points in the direction + * from {@code p1} to {@code p2}. + * @param p1 first point + * @param p2 second point + * @param precision precision context used to compare floating point values + * @return new line containing {@code p1} and {@code p2} and pointing in the direction + * from {@code p1} to {@code p2} + * @throws GeometryValueException If the vector between {@code p1} and {@code p2} has zero length, + * as evaluated by the given precision context + */ + public static Line fromPoints(final Vector2D p1, final Vector2D p2, final DoublePrecisionContext precision) { + return fromPointAndDirection(p1, p1.vectorTo(p2), precision); + } + + /** Create a line from a point and direction. + * @param pt point belonging to the line + * @param dir the direction of the line + * @param precision precision context used to compare floating point values + * @return new line containing {@code pt} and pointing in direction {@code dir} + * @throws GeometryValueException If {@code dir} has zero length, as evaluated by the + * given precision context + */ + public static Line fromPointAndDirection(final Vector2D pt, final Vector2D dir, final DoublePrecisionContext precision) { + if (dir.isZero(precision)) { + throw new GeometryValueException("Line direction cannot be zero"); + } - /** cXX * cY1 - cYX * cX1. */ - private final double c1X; + final Vector2D normalizedDir = dir.normalize(); + final double originOffset = normalizedDir.signedArea(pt); - /** cXX * cYY - cYX * cXY. */ - private final double c11; + return new Line(normalizedDir, originOffset, precision); + } - /** Build an affine line transform from a n {@code AffineTransform}. - * @param cXX transform factor between input abscissa and output abscissa - * @param cYX transform factor between input abscissa and output ordinate - * @param cXY transform factor between input ordinate and output abscissa - * @param cYY transform factor between input ordinate and output ordinate - * @param cX1 transform addendum for output abscissa - * @param cY1 transform addendum for output ordinate - * @exception IllegalArgumentException if the transform is non invertible - */ - LineTransform(final double cXX, final double cYX, final double cXY, - final double cYY, final double cX1, final double cY1) - throws IllegalArgumentException { + /** Create a line from a point lying on the line and an angle relative to the abscissa (x) axis. Note that the + * line does not need to intersect the x-axis; the given angle is simply relative to it. + * @param pt point belonging to the line + * @param angle angle of the line with respect to abscissa (x) axis, in radians + * @param precision precision context used to compare floating point values + * @return new line containing {@code pt} and forming the given angle with the + * abscissa (x) axis. + */ + public static Line fromPointAndAngle(final Vector2D pt, final double angle, final DoublePrecisionContext precision) { + final Vector2D dir = Vector2D.normalize(Math.cos(angle), Math.sin(angle)); + return fromPointAndDirection(pt, dir, precision); + } - this.cXX = cXX; - this.cYX = cYX; - this.cXY = cXY; - this.cYY = cYY; - this.cX1 = cX1; - this.cY1 = cY1; + // TODO: Remove this method and associated class after the Transform interface has been simplified. + // See GEOMETRY-24. + + /** Create a {@link Transform} instance from a set of column vectors. The returned object can be used + * to transform {@link SubLine} instances. + * @param u first column vector; this corresponds to the first basis vector + * in the coordinate frame + * @param v second column vector; this corresponds to the second basis vector + * in the coordinate frame + * @param t third column vector; this corresponds to the translation of the transform + * @return a new transform instance + */ + public static Transform<Vector2D, Vector1D> getTransform(final Vector2D u, final Vector2D v, final Vector2D t) { + final AffineTransformMatrix2D matrix = AffineTransformMatrix2D.fromColumnVectors(u, v, t); + return new LineTransform(matrix); + } - c1Y = LinearCombination.value(cXY, cY1, -cYY, cX1); - c1X = LinearCombination.value(cXX, cY1, -cYX, cX1); - c11 = LinearCombination.value(cXX, cYY, -cYX, cXY); + /** Class wrapping an {@link AffineTransformMatrix2D} with the methods necessary to fulfill the full + * {@link Transform} interface. + */ + private static class LineTransform implements Transform<Vector2D, Vector1D> { - if (Math.abs(c11) < 1.0e-20) { - throw new IllegalArgumentException("Non-invertible affine transform collapses some lines into single points"); - } + /** Transform matrix */ + private final AffineTransformMatrix2D matrix; + /** Simple constructor. + * @param matrix transform matrix + */ + LineTransform(final AffineTransformMatrix2D matrix) { + this.matrix = matrix; } /** {@inheritDoc} */ @Override public Vector2D apply(final Vector2D point) { - final double x = point.getX(); - final double y = point.getY(); - return Vector2D.of(LinearCombination.value(cXX, x, cXY, y, cX1, 1), - LinearCombination.value(cYX, x, cYY, y, cY1, 1)); + return matrix.apply(point); } /** {@inheritDoc} */ @Override public Line apply(final Hyperplane<Vector2D> hyperplane) { - final Line line = (Line) hyperplane; - final double rOffset = LinearCombination.value(c1X, line.cos, c1Y, line.sin, c11, line.originOffset); - final double rCos = LinearCombination.value(cXX, line.cos, cXY, line.sin); - final double rSin = LinearCombination.value(cYX, line.cos, cYY, line.sin); - final double inv = 1.0 / Math.sqrt(rSin * rSin + rCos * rCos); - return new Line(Math.PI + Math.atan2(-rSin, -rCos), - inv * rCos, inv * rSin, - inv * rOffset, line.precision); + final Line line = (Line) hyperplane; + return line.transform(matrix); } /** {@inheritDoc} */ @@ -491,14 +474,12 @@ public class Line implements Hyperplane<Vector2D>, Embedding<Vector2D, Vector1D> public SubHyperplane<Vector1D> apply(final SubHyperplane<Vector1D> sub, final Hyperplane<Vector2D> original, final Hyperplane<Vector2D> transformed) { - final OrientedPoint op = (OrientedPoint) sub.getHyperplane(); - final Line originalLine = (Line) original; + final OrientedPoint op = (OrientedPoint) sub.getHyperplane(); + final Line originalLine = (Line) original; final Line transformedLine = (Line) transformed; final Vector1D newLoc = transformedLine.toSubSpace(apply(originalLine.toSpace(op.getLocation()))); return OrientedPoint.fromPointAndDirection(newLoc, op.getDirection(), originalLine.precision).wholeHyperplane(); } - } - } diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/NestedLoops.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/NestedLoops.java index 6f15bd9..d6dbbf9 100644 --- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/NestedLoops.java +++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/NestedLoops.java @@ -93,7 +93,7 @@ class NestedLoops { for (int i = 0; i < loop.length; ++i) { final Vector2D previous = current; current = loop[i]; - final Line line = new Line(previous, current, precision); + final Line line = Line.fromPoints(previous, current, precision); final IntervalsSet region = new IntervalsSet(line.toSubSpace(previous).getX(), line.toSubSpace(current).getX(), diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/PolygonsSet.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/PolygonsSet.java index 2d43b1a..ee76ff9 100644 --- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/PolygonsSet.java +++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/PolygonsSet.java @@ -163,10 +163,10 @@ public class PolygonsSet extends AbstractRegion<Vector2D, Vector1D> { final Vector2D maxMin = Vector2D.of(xMax, yMin); final Vector2D maxMax = Vector2D.of(xMax, yMax); return new Line[] { - new Line(minMin, maxMin, precision), - new Line(maxMin, maxMax, precision), - new Line(maxMax, minMax, precision), - new Line(minMax, minMin, precision) + Line.fromPoints(minMin, maxMin, precision), + Line.fromPoints(maxMin, maxMax, precision), + Line.fromPoints(maxMax, minMax, precision), + Line.fromPoints(minMax, minMin, precision) }; } @@ -212,7 +212,7 @@ public class PolygonsSet extends AbstractRegion<Vector2D, Vector1D> { // with the current one Line line = start.sharedLineWith(end); if (line == null) { - line = new Line(start.getLocation(), end.getLocation(), precision); + line = Line.fromPoints(start.getLocation(), end.getLocation(), precision); } // create the edge and store it @@ -738,12 +738,12 @@ public class PolygonsSet extends AbstractRegion<Vector2D, Vector1D> { private int splitEdgeConnections(final List<ConnectableSegment> segments) { int connected = 0; for (final ConnectableSegment segment : segments) { - if (segment.getNext() == null) { + if (segment.getNext() == null && segment.getEndNode() != null) { final Hyperplane<Vector2D> hyperplane = segment.getNode().getCut().getHyperplane(); final BSPTree<Vector2D> end = segment.getEndNode(); for (final ConnectableSegment candidateNext : segments) { if (candidateNext.getPrevious() == null && - candidateNext.getNode().getCut().getHyperplane() == hyperplane && + candidateNext.getNode().getCut().getHyperplane().equals(hyperplane) && candidateNext.getStartNode() == end) { // connect the two segments segment.setNext(candidateNext); @@ -1054,7 +1054,7 @@ public class PolygonsSet extends AbstractRegion<Vector2D, Vector1D> { final BSPTree<Vector2D> endN = selectClosest(endV, splitters); if (reversed) { - segments.add(new ConnectableSegment(endV, startV, line.getReverse(), + segments.add(new ConnectableSegment(endV, startV, line.reverse(), node, endN, startN)); } else { segments.add(new ConnectableSegment(startV, endV, line, diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/SubLine.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/SubLine.java index 21b340b..3e07ffa 100644 --- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/SubLine.java +++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/SubLine.java @@ -50,7 +50,7 @@ public class SubLine extends AbstractSubHyperplane<Vector2D, Vector1D> { * @param precision precision context used to compare floating point values */ public SubLine(final Vector2D start, final Vector2D end, final DoublePrecisionContext precision) { - super(new Line(start, end, precision), buildIntervalSet(start, end, precision)); + super(Line.fromPoints(start, end, precision), buildIntervalSet(start, end, precision)); } /** Create a sub-line from a segment. @@ -138,7 +138,7 @@ public class SubLine extends AbstractSubHyperplane<Vector2D, Vector1D> { * @return an interval set */ private static IntervalsSet buildIntervalSet(final Vector2D start, final Vector2D end, final DoublePrecisionContext precision) { - final Line line = new Line(start, end, precision); + final Line line = Line.fromPoints(start, end, precision); return new IntervalsSet(line.toSubSpace(start).getX(), line.toSubSpace(end).getX(), precision); diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/core/partitioning/CharacterizationTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/core/partitioning/CharacterizationTest.java index 5f13fb4..f172467 100644 --- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/core/partitioning/CharacterizationTest.java +++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/core/partitioning/CharacterizationTest.java @@ -409,11 +409,11 @@ public class CharacterizationTest { } private Line buildLine(Vector2D p1, Vector2D p2) { - return new Line(p1, p2, TEST_PRECISION); + return Line.fromPoints(p1, p2, TEST_PRECISION); } private SubLine buildSubLine(Vector2D start, Vector2D end) { - Line line = new Line(start, end, TEST_PRECISION); + Line line = Line.fromPoints(start, end, TEST_PRECISION); double lower = (line.toSubSpace(start)).getX(); double upper = (line.toSubSpace(end)).getX(); return new SubLine(line, new IntervalsSet(lower, upper, TEST_PRECISION)); diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/EuclideanTestUtils.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/EuclideanTestUtils.java index 3fc4280..7d6bf18 100644 --- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/EuclideanTestUtils.java +++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/EuclideanTestUtils.java @@ -317,7 +317,7 @@ public class EuclideanTestUtils { @Override public Line parseHyperplane() throws ParseException { - return new Line(Vector2D.of(getNumber(), getNumber()), getNumber(), getPrecision()); + return Line.fromPointAndAngle(Vector2D.of(getNumber(), getNumber()), getNumber(), getPrecision()); } }; diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2DTest.java index b2612af..9235793 100644 --- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2DTest.java +++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2DTest.java @@ -53,6 +53,39 @@ public class AffineTransformMatrix2DTest { } @Test + public void testFromColumnVectors_twoVector() { + // arrange + Vector2D u = Vector2D.of(1, 2); + Vector2D v = Vector2D.of(3, 4); + + // act + AffineTransformMatrix2D transform = AffineTransformMatrix2D.fromColumnVectors(u, v); + + // assert + Assert.assertArrayEquals(new double[] { + 1, 3, 0, + 2, 4, 0 + }, transform.toArray(), 0.0); + } + + @Test + public void testFromColumnVectors_threeVectors() { + // arrange + Vector2D u = Vector2D.of(1, 2); + Vector2D v = Vector2D.of(3, 4); + Vector2D t = Vector2D.of(5, 6); + + // act + AffineTransformMatrix2D transform = AffineTransformMatrix2D.fromColumnVectors(u, v, t); + + // assert + Assert.assertArrayEquals(new double[] { + 1, 3, 5, + 2, 4, 6 + }, transform.toArray(), 0.0); + } + + @Test public void testIdentity() { // act AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity(); diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LineTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LineTest.java index 5a5db25..0e26c92 100644 --- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LineTest.java +++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LineTest.java @@ -16,10 +16,15 @@ */ package org.apache.commons.geometry.euclidean.twod; +import org.apache.commons.geometry.core.Geometry; +import org.apache.commons.geometry.core.GeometryTestUtils; +import org.apache.commons.geometry.core.exception.GeometryValueException; import org.apache.commons.geometry.core.partitioning.Transform; import org.apache.commons.geometry.core.precision.DoublePrecisionContext; import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext; +import org.apache.commons.geometry.euclidean.EuclideanTestUtils; import org.apache.commons.geometry.euclidean.oned.Vector1D; +import org.apache.commons.numbers.angle.PlaneAngleRadians; import org.junit.Assert; import org.junit.Test; @@ -31,106 +36,974 @@ public class LineTest { new EpsilonDoublePrecisionContext(TEST_EPS); @Test - public void testContains() { - Line l = new Line(Vector2D.of(0, 1), Vector2D.of(1, 2), TEST_PRECISION); - Assert.assertTrue(l.contains(Vector2D.of(0, 1))); - Assert.assertTrue(l.contains(Vector2D.of(1, 2))); - Assert.assertTrue(l.contains(Vector2D.of(7, 8))); - Assert.assertTrue(! l.contains(Vector2D.of(8, 7))); + public void testFromPoints() { + // act/assert + checkLine(Line.fromPoints(Vector2D.ZERO, Vector2D.PLUS_X, TEST_PRECISION), + Vector2D.ZERO, Vector2D.PLUS_X); + checkLine(Line.fromPoints(Vector2D.ZERO, Vector2D.of(100, 0), TEST_PRECISION), + Vector2D.ZERO, Vector2D.PLUS_X); + checkLine(Line.fromPoints(Vector2D.of(100, 0), Vector2D.ZERO, TEST_PRECISION), + Vector2D.ZERO, Vector2D.MINUS_X); + checkLine(Line.fromPoints(Vector2D.of(-100, 0), Vector2D.of(100, 0), TEST_PRECISION), + Vector2D.ZERO, Vector2D.PLUS_X); + + checkLine(Line.fromPoints(Vector2D.of(-2, 0), Vector2D.of(0, 2), TEST_PRECISION), + Vector2D.of(-1, 1), Vector2D.of(1, 1).normalize()); + checkLine(Line.fromPoints(Vector2D.of(0, 2), Vector2D.of(-2, 0), TEST_PRECISION), + Vector2D.of(-1, 1), Vector2D.of(-1, -1).normalize()); + } + + @Test + public void testFromPoints_pointsTooClose() { + // act/assert + GeometryTestUtils.assertThrows(() -> Line.fromPoints(Vector2D.PLUS_X, Vector2D.PLUS_X, TEST_PRECISION), + GeometryValueException.class, "Line direction cannot be zero"); + GeometryTestUtils.assertThrows(() -> Line.fromPoints(Vector2D.PLUS_X, Vector2D.of(1 + 1e-11, 1e-11), TEST_PRECISION), + GeometryValueException.class, "Line direction cannot be zero"); + } + + @Test + public void testFromPointAndDirection() { + // act/assert + checkLine(Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.PLUS_X, TEST_PRECISION), + Vector2D.ZERO, Vector2D.PLUS_X); + checkLine(Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(100, 0), TEST_PRECISION), + Vector2D.ZERO, Vector2D.PLUS_X); + checkLine(Line.fromPointAndDirection(Vector2D.of(-100, 0), Vector2D.of(100, 0), TEST_PRECISION), + Vector2D.ZERO, Vector2D.PLUS_X); + + checkLine(Line.fromPointAndDirection(Vector2D.of(-2, 0), Vector2D.of(1, 1), TEST_PRECISION), + Vector2D.of(-1, 1), Vector2D.of(1, 1).normalize()); + checkLine(Line.fromPointAndDirection(Vector2D.of(0, 2), Vector2D.of(-1, -1), TEST_PRECISION), + Vector2D.of(-1, 1), Vector2D.of(-1, -1).normalize()); + } + + @Test + public void testFromPointAndDirection_directionIsZero() { + // act/assert + GeometryTestUtils.assertThrows(() -> Line.fromPointAndDirection(Vector2D.PLUS_X, Vector2D.ZERO, TEST_PRECISION), + GeometryValueException.class, "Line direction cannot be zero"); + GeometryTestUtils.assertThrows(() -> Line.fromPointAndDirection(Vector2D.PLUS_X, Vector2D.of(1e-11, -1e-12), TEST_PRECISION), + GeometryValueException.class, "Line direction cannot be zero"); + } + + @Test + public void testFromPointAndAngle() { + // act/assert + checkLine(Line.fromPointAndAngle(Vector2D.ZERO, 0, TEST_PRECISION), + Vector2D.ZERO, Vector2D.PLUS_X); + checkLine(Line.fromPointAndAngle(Vector2D.of(1, 1), Geometry.HALF_PI, TEST_PRECISION), + Vector2D.of(1, 0), Vector2D.PLUS_Y); + checkLine(Line.fromPointAndAngle(Vector2D.of(-1, -1), Geometry.PI, TEST_PRECISION), + Vector2D.of(0, -1), Vector2D.MINUS_X); + checkLine(Line.fromPointAndAngle(Vector2D.of(1, -1), Geometry.MINUS_HALF_PI, TEST_PRECISION), + Vector2D.of(1, 0), Vector2D.MINUS_Y); + checkLine(Line.fromPointAndAngle(Vector2D.of(-1, 1), Geometry.TWO_PI, TEST_PRECISION), + Vector2D.of(0, 1), Vector2D.PLUS_X); + } + + @Test + public void testGetAngle() { + // arrange + Vector2D vec = Vector2D.of(1, 2); + + for (double theta = -4 * Geometry.PI; theta < 2 * Geometry.PI; theta += 0.1) { + Line line = Line.fromPointAndAngle(vec, theta, TEST_PRECISION); + + // act/assert + Assert.assertEquals(PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(theta), + line.getAngle(), TEST_EPS); + } + } + + @Test + public void testGetAngle_multiplesOfPi() { + // arrange + Vector2D vec = Vector2D.of(-1, -2); + + // act/assert + Assert.assertEquals(0, Line.fromPointAndAngle(vec, Geometry.ZERO_PI, TEST_PRECISION).getAngle(), TEST_EPS); + Assert.assertEquals(Geometry.PI, Line.fromPointAndAngle(vec, Geometry.PI, TEST_PRECISION).getAngle(), TEST_EPS); + Assert.assertEquals(0, Line.fromPointAndAngle(vec, Geometry.TWO_PI, TEST_PRECISION).getAngle(), TEST_EPS); + + Assert.assertEquals(0, Line.fromPointAndAngle(vec, -2 * Geometry.PI, TEST_PRECISION).getAngle(), TEST_EPS); + Assert.assertEquals(Geometry.PI, Line.fromPointAndAngle(vec, -3 * Geometry.PI, TEST_PRECISION).getAngle(), TEST_EPS); + Assert.assertEquals(0, Line.fromPointAndAngle(vec, -4 * Geometry.TWO_PI, TEST_PRECISION).getAngle(), TEST_EPS); + } + + @Test + public void testGetDirection() { + // act/assert + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.PLUS_X, + Line.fromPoints(Vector2D.of(0, 0), Vector2D.of(1, 0), TEST_PRECISION).getDirection(), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.MINUS_Y, + Line.fromPoints(Vector2D.of(0, 1), Vector2D.of(0, -1), TEST_PRECISION).getDirection(), TEST_EPS); + + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.MINUS_X, + Line.fromPoints(Vector2D.of(2, 2), Vector2D.of(1, 2), TEST_PRECISION).getDirection(), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.PLUS_X, + Line.fromPoints(Vector2D.of(10, -2), Vector2D.of(10.1, -2), TEST_PRECISION).getDirection(), TEST_EPS); + + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.MINUS_Y, + Line.fromPoints(Vector2D.of(3, 2), Vector2D.of(3, 1), TEST_PRECISION).getDirection(), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.PLUS_Y, + Line.fromPoints(Vector2D.of(-3, 10), Vector2D.of(-3, 10.1), TEST_PRECISION).getDirection(), TEST_EPS); + + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, -1).normalize(), + Line.fromPoints(Vector2D.of(0, 2), Vector2D.of(2, 0), TEST_PRECISION).getDirection(), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, 1).normalize(), + Line.fromPoints(Vector2D.of(2, 0), Vector2D.of(0, 2), TEST_PRECISION).getDirection(), TEST_EPS); } @Test - public void testAbscissa() { - Line l = new Line(Vector2D.of(2, 1), Vector2D.of(-2, -2), TEST_PRECISION); + public void testGetOffsetDirection() { + // act/assert + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.MINUS_Y, + Line.fromPoints(Vector2D.of(0, 0), Vector2D.of(1, 0), TEST_PRECISION).getOffsetDirection(), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.MINUS_X, + Line.fromPoints(Vector2D.of(0, 1), Vector2D.of(0, -1), TEST_PRECISION).getOffsetDirection(), TEST_EPS); + + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.PLUS_Y, + Line.fromPoints(Vector2D.of(2, 2), Vector2D.of(1, 2), TEST_PRECISION).getOffsetDirection(), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.MINUS_Y, + Line.fromPoints(Vector2D.of(10, -2), Vector2D.of(10.1, -2), TEST_PRECISION).getOffsetDirection(), TEST_EPS); + + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.MINUS_X, + Line.fromPoints(Vector2D.of(3, 2), Vector2D.of(3, 1), TEST_PRECISION).getOffsetDirection(), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.PLUS_X, + Line.fromPoints(Vector2D.of(-3, 10), Vector2D.of(-3, 10.1), TEST_PRECISION).getOffsetDirection(), TEST_EPS); + + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, -1).normalize(), + Line.fromPoints(Vector2D.of(0, 2), Vector2D.of(2, 0), TEST_PRECISION).getOffsetDirection(), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1).normalize(), + Line.fromPoints(Vector2D.of(2, 0), Vector2D.of(0, 2), TEST_PRECISION).getOffsetDirection(), TEST_EPS); + } + + @Test + public void testGetOrigin() { + // act/assert + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, + Line.fromPoints(Vector2D.of(0, 0), Vector2D.of(1, 0), TEST_PRECISION).getOrigin(), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, + Line.fromPoints(Vector2D.of(0, 1), Vector2D.of(0, -1), TEST_PRECISION).getOrigin(), TEST_EPS); + + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, 2), + Line.fromPoints(Vector2D.of(2, 2), Vector2D.of(3, 2), TEST_PRECISION).getOrigin(), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, -2), + Line.fromPoints(Vector2D.of(10, -2), Vector2D.of(10.1, -2), TEST_PRECISION).getOrigin(), TEST_EPS); + + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(3, 0), + Line.fromPoints(Vector2D.of(3, 2), Vector2D.of(3, 1), TEST_PRECISION).getOrigin(), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-3, 0), + Line.fromPoints(Vector2D.of(-3, 10), Vector2D.of(-3, 10.1), TEST_PRECISION).getOrigin(), TEST_EPS); + + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), + Line.fromPoints(Vector2D.of(0, 2), Vector2D.of(2, 0), TEST_PRECISION).getOrigin(), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), + Line.fromPoints(Vector2D.of(2, 0), Vector2D.of(0, 2), TEST_PRECISION).getOrigin(), TEST_EPS); + } + + @Test + public void testGetOriginOffset() { + // arrange + double sqrt2 = Math.sqrt(2); + + // act/assert Assert.assertEquals(0.0, - (l.toSubSpace(Vector2D.of(-3, 4))).getX(), - 1.0e-10); + Line.fromPoints(Vector2D.of(0, 0), Vector2D.of(1, 1), TEST_PRECISION).getOriginOffset(), TEST_EPS); Assert.assertEquals(0.0, - (l.toSubSpace(Vector2D.of( 3, -4))).getX(), - 1.0e-10); - Assert.assertEquals(-5.0, - (l.toSubSpace(Vector2D.of( 7, -1))).getX(), - 1.0e-10); - Assert.assertEquals(5.0, - (l.toSubSpace(Vector2D.of(-1, -7))).getX(), - 1.0e-10); + Line.fromPoints(Vector2D.of(0, 0), Vector2D.of(-1, -1), TEST_PRECISION).getOriginOffset(), TEST_EPS); + + Assert.assertEquals(sqrt2, + Line.fromPoints(Vector2D.of(-1, 1), Vector2D.of(0, 2), TEST_PRECISION).getOriginOffset(), TEST_EPS); + Assert.assertEquals(-sqrt2, + Line.fromPoints(Vector2D.of(0, -2), Vector2D.of(1, -1), TEST_PRECISION).getOriginOffset(), TEST_EPS); + + Assert.assertEquals(-sqrt2, + Line.fromPoints(Vector2D.of(0, 2), Vector2D.of(-1, 1), TEST_PRECISION).getOriginOffset(), TEST_EPS); + Assert.assertEquals(sqrt2, + Line.fromPoints(Vector2D.of(1, -1), Vector2D.of(0, -2), TEST_PRECISION).getOriginOffset(), TEST_EPS); + } + + @Test + public void testGetPrecision() { + // act/assert + Assert.assertSame(TEST_PRECISION, Line.fromPoints(Vector2D.ZERO, Vector2D.PLUS_X, TEST_PRECISION).getPrecision()); + Assert.assertSame(TEST_PRECISION, Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.PLUS_X, TEST_PRECISION).getPrecision()); + Assert.assertSame(TEST_PRECISION, Line.fromPointAndAngle(Vector2D.ZERO, 0, TEST_PRECISION).getPrecision()); + } + + @Test + public void testCopySelf() { + // arrange + Line line = Line.fromPoints(Vector2D.ZERO, Vector2D.PLUS_X, TEST_PRECISION); + + // act/assert + Assert.assertSame(line, line.copySelf()); + } + + @Test + public void testReverse() { + // arrange + Vector2D pt = Vector2D.of(0, 1); + Vector2D dir = Vector2D.PLUS_X; + Line line = Line.fromPointAndDirection(pt, dir, TEST_PRECISION); + + // act + Line reversed = line.reverse(); + Line doubleReversed = reversed.reverse(); + + // assert + checkLine(reversed, pt, dir.negate()); + Assert.assertEquals(-1, reversed.getOriginOffset(), TEST_EPS); + + checkLine(doubleReversed, pt, dir); + Assert.assertEquals(1, doubleReversed.getOriginOffset(), TEST_EPS); + } + + @Test + public void testToSubSpace() { + // arrange + Line line = Line.fromPoints(Vector2D.of(2, 1), Vector2D.of(-2, -2), TEST_PRECISION); + + // act/assert + Assert.assertEquals(0.0, line.toSubSpace(Vector2D.of(-3, 4)).getX(), TEST_EPS); + Assert.assertEquals(0.0, line.toSubSpace(Vector2D.of( 3, -4)).getX(), TEST_EPS); + Assert.assertEquals(-5.0, line.toSubSpace(Vector2D.of(7, -1)).getX(), TEST_EPS); + Assert.assertEquals(5.0, line.toSubSpace(Vector2D.of(-1, -7)).getX(), TEST_EPS); + } + + @Test + public void testToSpace_throughOrigin() { + // arrange + double invSqrt2 = 1 / Math.sqrt(2); + Vector2D dir = Vector2D.of(invSqrt2, invSqrt2); + + Line line = Line.fromPoints(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION); + + // act/assert + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, line.toSpace(Vector1D.of(0)), TEST_EPS); + + for (int i=0; i<100; ++i) { + EuclideanTestUtils.assertCoordinatesEqual(dir.multiply(i), line.toSpace(Vector1D.of(i)), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(dir.multiply(-i), line.toSpace(Vector1D.of(-i)), TEST_EPS); + } + } + + @Test + public void testToSpace_offsetFromOrigin() { + // arrange + double angle = Geometry.PI / 6; + double cos = Math.cos(angle); + double sin = Math.sin(angle); + Vector2D pt = Vector2D.of(-5, 0); + + double h = Math.abs(pt.getX()) * cos; + double d = h * cos; + Vector2D origin = Vector2D.of( + pt.getX() + d, + h * sin + ); + Vector2D dir = Vector2D.of(cos, sin); + + Line line = Line.fromPointAndAngle(pt, angle, TEST_PRECISION); + + // act/assert + EuclideanTestUtils.assertCoordinatesEqual(origin, line.toSpace(Vector1D.of(0)), TEST_EPS); + + for (int i=0; i<100; ++i) { + EuclideanTestUtils.assertCoordinatesEqual(origin.add(dir.multiply(i)), line.toSpace(Vector1D.of(i)), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(origin.add(dir.multiply(-i)), line.toSpace(Vector1D.of(-i)), TEST_EPS); + } + } + + @Test + public void testIntersection() { + // arrange + Line a = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.PLUS_X, TEST_PRECISION); + Line b = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.PLUS_Y, TEST_PRECISION); + Line c = Line.fromPointAndDirection(Vector2D.of(0, 2), Vector2D.of(2, 1), TEST_PRECISION); + Line d = Line.fromPointAndDirection(Vector2D.of(0, -1), Vector2D.of(2, -1), TEST_PRECISION); + + // act/assert + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, a.intersection(b), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, b.intersection(a), TEST_EPS); + + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-4, 0), a.intersection(c), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-4, 0), c.intersection(a), TEST_EPS); + + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-2, 0), a.intersection(d), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-2, 0), d.intersection(a), TEST_EPS); + + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, 2), b.intersection(c), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, 2), c.intersection(b), TEST_EPS); + + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, -1), b.intersection(d), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, -1), d.intersection(b), TEST_EPS); + + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-3, 0.5), c.intersection(d), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-3, 0.5), d.intersection(c), TEST_EPS); + } + + @Test + public void testIntersection_parallel() { + // arrange + Line a = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.PLUS_X, TEST_PRECISION); + Line b = Line.fromPointAndDirection(Vector2D.of(0, 1), Vector2D.PLUS_X, TEST_PRECISION); + + Line c = Line.fromPointAndDirection(Vector2D.of(0, 2), Vector2D.of(2, 1), TEST_PRECISION); + Line d = Line.fromPointAndDirection(Vector2D.of(0, -1), Vector2D.of(2, 1), TEST_PRECISION); + + // act/assert + Assert.assertNull(a.intersection(b)); + Assert.assertNull(b.intersection(a)); + + Assert.assertNull(c.intersection(d)); + Assert.assertNull(d.intersection(c)); + } + + @Test + public void testIntersection_coincident() { + // arrange + Line a = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.PLUS_X, TEST_PRECISION); + Line b = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.PLUS_X, TEST_PRECISION); + + Line c = Line.fromPointAndDirection(Vector2D.of(0, 2), Vector2D.of(2, 1), TEST_PRECISION); + Line d = Line.fromPointAndDirection(Vector2D.of(0, 2), Vector2D.of(2, 1), TEST_PRECISION); + + // act/assert + Assert.assertNull(a.intersection(b)); + Assert.assertNull(b.intersection(a)); + + Assert.assertNull(c.intersection(d)); + Assert.assertNull(d.intersection(c)); + } + + @Test + public void testProject() { + // --- arrange + Line xAxis = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.PLUS_X, TEST_PRECISION); + Line yAxis = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.PLUS_Y, TEST_PRECISION); + + double diagonalYIntercept = 1; + Vector2D diagonalDir = Vector2D.of(1, 2); + Line diagonal = Line.fromPointAndDirection(Vector2D.of(0, diagonalYIntercept), diagonalDir, TEST_PRECISION); + + EuclideanTestUtils.permute(-5, 5, 0.5, (x, y) -> { + Vector2D pt = Vector2D.of(x, y); + + // --- act/assert + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(x, 0), xAxis.project(pt), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, y), yAxis.project(pt), TEST_EPS); + + Vector2D diagonalPt = diagonal.project(pt); + Assert.assertTrue(diagonal.contains(diagonalPt)); + Assert.assertEquals(diagonal.distance(pt), pt.distance(diagonalPt), TEST_EPS); + + // check that y = mx + b is true + Assert.assertEquals(diagonalPt.getY(), + (diagonalDir.getY() * diagonalPt.getX() / diagonalDir.getX()) + diagonalYIntercept, TEST_EPS); + }); + } + + @Test + public void testWholeHyperplane() { + // arrange + Line line = Line.fromPoints(Vector2D.ZERO, Vector2D.PLUS_X, TEST_PRECISION); + + // act + SubLine result = line.wholeHyperplane(); + + // assert + Assert.assertSame(line, result.getHyperplane()); + GeometryTestUtils.assertPositiveInfinity(result.getSize()); + } + + @Test + public void testWholeSpace() { + // arrange + Line line = Line.fromPoints(Vector2D.ZERO, Vector2D.PLUS_X, TEST_PRECISION); + + // act + PolygonsSet result = line.wholeSpace(); + + // assert + GeometryTestUtils.assertPositiveInfinity(result.getSize()); + Assert.assertSame(TEST_PRECISION, result.getPrecision()); + } + + @Test + public void testGetOffset_parallelLines() { + // arrange + double dist = Math.sin(Math.atan2(2, 1)); + + Line a = Line.fromPoints(Vector2D.of(-2, 0), Vector2D.of(0, 4), TEST_PRECISION); + Line b = Line.fromPoints(Vector2D.of(-3, 0), Vector2D.of(0, 6), TEST_PRECISION); + Line c = Line.fromPoints(Vector2D.of(-1, 0), Vector2D.of(0, 2), TEST_PRECISION); + Line d = Line.fromPoints(Vector2D.of(1, 0), Vector2D.of(0, -2), TEST_PRECISION); + + // act/assert + Assert.assertEquals(-dist, a.getOffset(b), TEST_EPS); + Assert.assertEquals(dist, b.getOffset(a), TEST_EPS); + + Assert.assertEquals(dist, a.getOffset(c), TEST_EPS); + Assert.assertEquals(-dist, c.getOffset(a), TEST_EPS); + + Assert.assertEquals(3 * dist, a.getOffset(d), TEST_EPS); + Assert.assertEquals(3 * dist, d.getOffset(a), TEST_EPS); + } + + @Test + public void testGetOffset_coincidentLines() { + // arrange + Line a = Line.fromPoints(Vector2D.of(-2, 0), Vector2D.of(0, 4), TEST_PRECISION); + Line b = Line.fromPoints(Vector2D.of(-2, 0), Vector2D.of(0, 4), TEST_PRECISION); + Line c = b.reverse(); + + // act/assert + Assert.assertEquals(0, a.getOffset(a), TEST_EPS); + + Assert.assertEquals(0, a.getOffset(b), TEST_EPS); + Assert.assertEquals(0, b.getOffset(a), TEST_EPS); + + Assert.assertEquals(0, a.getOffset(c), TEST_EPS); + Assert.assertEquals(0, c.getOffset(a), TEST_EPS); + } + + @Test + public void testGetOffset_nonParallelLines() { + // arrange + Line a = Line.fromPoints(Vector2D.ZERO, Vector2D.PLUS_X, TEST_PRECISION); + Line b = Line.fromPoints(Vector2D.ZERO, Vector2D.PLUS_Y, TEST_PRECISION); + Line c = Line.fromPoints(Vector2D.of(-1, 0), Vector2D.of(0, 2), TEST_PRECISION); + Line d = Line.fromPoints(Vector2D.of(1, 0), Vector2D.of(0, 4), TEST_PRECISION); + + // act/assert + Assert.assertEquals(0, a.getOffset(b), TEST_EPS); + Assert.assertEquals(0, b.getOffset(a), TEST_EPS); + + Assert.assertEquals(0, a.getOffset(c), TEST_EPS); + Assert.assertEquals(0, c.getOffset(a), TEST_EPS); + + Assert.assertEquals(0, a.getOffset(d), TEST_EPS); + Assert.assertEquals(0, d.getOffset(a), TEST_EPS); + } + + @Test + public void testGetOffset_point() { + // arrange + Line line = Line.fromPoints(Vector2D.of(-1, 0), Vector2D.of(0, 2), TEST_PRECISION); + Line reversed = line.reverse(); + + // act/assert + Assert.assertEquals(0.0, line.getOffset(Vector2D.of(-0.5, 1)), TEST_EPS); + Assert.assertEquals(0.0, line.getOffset(Vector2D.of(-1.5, -1)), TEST_EPS); + Assert.assertEquals(0.0, line.getOffset(Vector2D.of(0.5, 3)), TEST_EPS); + + double d = Math.sin(Math.atan2(2, 1)); + + Assert.assertEquals(d, line.getOffset(Vector2D.ZERO), TEST_EPS); + Assert.assertEquals(-d, line.getOffset(Vector2D.of(-1, 2)), TEST_EPS); + + Assert.assertEquals(-d, reversed.getOffset(Vector2D.ZERO), TEST_EPS); + Assert.assertEquals(d, reversed.getOffset(Vector2D.of(-1, 2)), TEST_EPS); + } + + @Test + public void testGetOffset_point_permute() { + // arrange + Line line = Line.fromPoints(Vector2D.of(-1, 0), Vector2D.of(0, 2), TEST_PRECISION); + Vector2D lineOrigin = line.getOrigin(); + + EuclideanTestUtils.permute(-5, 5, 0.5, (x, y) -> { + Vector2D pt = Vector2D.of(x, y); + + // act + double offset = line.getOffset(pt); + + // arrange + Vector2D vec = lineOrigin.vectorTo(pt).reject(line.getDirection()); + double dot = vec.dot(line.getOffsetDirection()); + double expected = Math.signum(dot) * vec.norm(); + + Assert.assertEquals(expected, offset, TEST_EPS); + }); + } + + @Test + public void testSameOrientationAs() { + // arrange + Line a = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION); + Line b = Line.fromPointAndAngle(Vector2D.of(4, 5), Geometry.ZERO_PI, TEST_PRECISION); + Line c = Line.fromPointAndAngle(Vector2D.of(-1, -3), 0.4 * Geometry.PI, TEST_PRECISION); + Line d = Line.fromPointAndAngle(Vector2D.of(1, 0), -0.4 * Geometry.PI, TEST_PRECISION); + + Line e = Line.fromPointAndAngle(Vector2D.of(6, -3), Geometry.PI, TEST_PRECISION); + Line f = Line.fromPointAndAngle(Vector2D.of(8, 5), 0.8 * Geometry.PI, TEST_PRECISION); + Line g = Line.fromPointAndAngle(Vector2D.of(6, -3), -0.8 * Geometry.PI, TEST_PRECISION); + + // act/assert + Assert.assertTrue(a.sameOrientationAs(a)); + Assert.assertTrue(a.sameOrientationAs(b)); + Assert.assertTrue(b.sameOrientationAs(a)); + Assert.assertTrue(a.sameOrientationAs(c)); + Assert.assertTrue(c.sameOrientationAs(a)); + Assert.assertTrue(a.sameOrientationAs(d)); + Assert.assertTrue(d.sameOrientationAs(a)); + + Assert.assertFalse(c.sameOrientationAs(d)); + Assert.assertFalse(d.sameOrientationAs(c)); + + Assert.assertTrue(e.sameOrientationAs(f)); + Assert.assertTrue(f.sameOrientationAs(e)); + Assert.assertTrue(e.sameOrientationAs(g)); + Assert.assertTrue(g.sameOrientationAs(e)); + + Assert.assertFalse(a.sameOrientationAs(e)); + Assert.assertFalse(e.sameOrientationAs(a)); + } + + @Test + public void testSameOrientationAs_orthogonal() { + // arrange + Line a = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.PLUS_X, TEST_PRECISION); + Line b = Line.fromPointAndDirection(Vector2D.of(4, 5), Vector2D.PLUS_Y, TEST_PRECISION); + Line c = Line.fromPointAndDirection(Vector2D.of(-4, -5), Vector2D.MINUS_Y, TEST_PRECISION); + + // act/assert + Assert.assertTrue(a.sameOrientationAs(b)); + Assert.assertTrue(b.sameOrientationAs(a)); + Assert.assertTrue(a.sameOrientationAs(c)); + Assert.assertTrue(c.sameOrientationAs(a)); + } + + @Test + public void testDistance_parallelLines() { + // arrange + double dist = Math.sin(Math.atan2(2, 1)); + + Line a = Line.fromPoints(Vector2D.of(-2, 0), Vector2D.of(0, 4), TEST_PRECISION); + Line b = Line.fromPoints(Vector2D.of(-3, 0), Vector2D.of(0, 6), TEST_PRECISION); + Line c = Line.fromPoints(Vector2D.of(-1, 0), Vector2D.of(0, 2), TEST_PRECISION); + Line d = Line.fromPoints(Vector2D.of(1, 0), Vector2D.of(0, -2), TEST_PRECISION); + + // act/assert + Assert.assertEquals(dist, a.distance(b), TEST_EPS); + Assert.assertEquals(dist, b.distance(a), TEST_EPS); + + Assert.assertEquals(dist, a.distance(c), TEST_EPS); + Assert.assertEquals(dist, c.distance(a), TEST_EPS); + + Assert.assertEquals(3 * dist, a.distance(d), TEST_EPS); + Assert.assertEquals(3 * dist, d.distance(a), TEST_EPS); } @Test - public void testOffset() { - Line l = new Line(Vector2D.of(2, 1), Vector2D.of(-2, -2), TEST_PRECISION); - Assert.assertEquals(-5.0, l.getOffset(Vector2D.of(5, -3)), 1.0e-10); - Assert.assertEquals(+5.0, l.getOffset(Vector2D.of(-5, 2)), 1.0e-10); + public void testDistance_coincidentLines() { + // arrange + Line a = Line.fromPoints(Vector2D.of(-2, 0), Vector2D.of(0, 4), TEST_PRECISION); + Line b = Line.fromPoints(Vector2D.of(-2, 0), Vector2D.of(0, 4), TEST_PRECISION); + Line c = b.reverse(); + + // act/assert + Assert.assertEquals(0, a.distance(a), TEST_EPS); + + Assert.assertEquals(0, a.distance(b), TEST_EPS); + Assert.assertEquals(0, b.distance(a), TEST_EPS); + + Assert.assertEquals(0, a.distance(c), TEST_EPS); + Assert.assertEquals(0, c.distance(a), TEST_EPS); + } + + @Test + public void testDistance_nonParallelLines() { + // arrange + Line a = Line.fromPoints(Vector2D.ZERO, Vector2D.PLUS_X, TEST_PRECISION); + Line b = Line.fromPoints(Vector2D.ZERO, Vector2D.PLUS_Y, TEST_PRECISION); + Line c = Line.fromPoints(Vector2D.of(-1, 0), Vector2D.of(0, 2), TEST_PRECISION); + Line d = Line.fromPoints(Vector2D.of(1, 0), Vector2D.of(0, 4), TEST_PRECISION); + + // act/assert + Assert.assertEquals(0, a.distance(b), TEST_EPS); + Assert.assertEquals(0, b.distance(a), TEST_EPS); + + Assert.assertEquals(0, a.distance(c), TEST_EPS); + Assert.assertEquals(0, c.distance(a), TEST_EPS); + + Assert.assertEquals(0, a.distance(d), TEST_EPS); + Assert.assertEquals(0, d.distance(a), TEST_EPS); } @Test public void testDistance() { - Line l = new Line(Vector2D.of(2, 1), Vector2D.of(-2, -2), TEST_PRECISION); - Assert.assertEquals(+5.0, l.distance(Vector2D.of(5, -3)), 1.0e-10); - Assert.assertEquals(+5.0, l.distance(Vector2D.of(-5, 2)), 1.0e-10); + // arrange + Line line = Line.fromPoints(Vector2D.of(2, 1), Vector2D.of(-2, -2), TEST_PRECISION); + + // act/assert + Assert.assertEquals(0, line.distance(line.getOrigin()), TEST_EPS); + Assert.assertEquals(+5.0, line.distance(Vector2D.of(5, -3)), TEST_EPS); + Assert.assertEquals(+5.0, line.distance(Vector2D.of(-5, 2)), TEST_EPS); } @Test public void testPointAt() { - Line l = new Line(Vector2D.of(2, 1), Vector2D.of(-2, -2), TEST_PRECISION); - for (double a = -2.0; a < 2.0; a += 0.2) { - Vector1D pA = Vector1D.of(a); - Vector2D point = l.toSpace(pA); - Assert.assertEquals(a, (l.toSubSpace(point)).getX(), 1.0e-10); - Assert.assertEquals(0.0, l.getOffset(point), 1.0e-10); - for (double o = -2.0; o < 2.0; o += 0.2) { - point = l.getPointAt(pA, o); - Assert.assertEquals(a, (l.toSubSpace(point)).getX(), 1.0e-10); - Assert.assertEquals(o, l.getOffset(point), 1.0e-10); + // arrange + Vector2D origin = Vector2D.of(-1, 1); + double d = Math.sqrt(2); + Line line = Line.fromPointAndDirection(origin, Vector2D.of(1, 1), TEST_PRECISION); + + // act/assert + EuclideanTestUtils.assertCoordinatesEqual(origin, line.pointAt(0, 0), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, line.pointAt(0, d), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-2, 2), line.pointAt(0, -d), TEST_EPS); + + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-2, 0), line.pointAt(-d, 0), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, 2), line.pointAt(d, 0), TEST_EPS); + + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), line.pointAt(d, d), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-3, 1), line.pointAt(-d, -d), TEST_EPS); + } + + @Test + public void testPointAt_abscissaOffsetRoundtrip() { + // arrange + Line line = Line.fromPoints(Vector2D.of(2, 1), Vector2D.of(-2, -2), TEST_PRECISION); + + for (double abscissa = -2.0; abscissa < 2.0; abscissa += 0.2) { + for (double offset = -2.0; offset < 2.0; offset += 0.2) { + + // act + Vector2D point = line.pointAt(abscissa, offset); + + // assert + Assert.assertEquals(abscissa, line.toSubSpace(point).getX(), TEST_EPS); + Assert.assertEquals(offset, line.getOffset(point), TEST_EPS); } } } @Test - public void testOriginOffset() { - Line l1 = new Line(Vector2D.of(0, 1), Vector2D.of(1, 2), TEST_PRECISION); - Assert.assertEquals(Math.sqrt(0.5), l1.getOriginOffset(), 1.0e-10); - Line l2 = new Line(Vector2D.of(1, 2), Vector2D.of(0, 1), TEST_PRECISION); - Assert.assertEquals(-Math.sqrt(0.5), l2.getOriginOffset(), 1.0e-10); + public void testContains_line() { + // arrange + Vector2D pt = Vector2D.of(1, 2); + Vector2D dir = Vector2D.of(3, 7); + Line a = Line.fromPointAndDirection(pt, dir, TEST_PRECISION); + Line b = Line.fromPointAndDirection(Vector2D.of(0, -4), dir, TEST_PRECISION); + Line c = Line.fromPointAndDirection(Vector2D.of(-2, -2), dir.negate(), TEST_PRECISION); + Line d = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.PLUS_X, TEST_PRECISION); + + Line e = Line.fromPointAndDirection(pt, dir, TEST_PRECISION); + Line f = Line.fromPointAndDirection(pt, dir.negate(), TEST_PRECISION); + + // act/assert + Assert.assertTrue(a.contains(a)); + + Assert.assertTrue(a.contains(e)); + Assert.assertTrue(e.contains(a)); + + Assert.assertTrue(a.contains(f)); + Assert.assertTrue(f.contains(a)); + + Assert.assertFalse(a.contains(b)); + Assert.assertFalse(a.contains(c)); + Assert.assertFalse(a.contains(d)); + } + + @Test + public void testIsParallel_closeToEpsilon() { + // arrange + double eps = 1e-3; + DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(eps); + + Vector2D p = Vector2D.of(1, 2); + + Line line = Line.fromPointAndAngle(p, Geometry.ZERO_PI, precision); + + // act/assert + Vector2D offset1 = Vector2D.of(0, 1e-4); + Vector2D offset2 = Vector2D.of(0, 2e-3); + + Assert.assertTrue(line.contains(Line.fromPointAndAngle(p.add(offset1), Geometry.ZERO_PI, precision))); + Assert.assertTrue(line.contains(Line.fromPointAndAngle(p.subtract(offset1), Geometry.ZERO_PI, precision))); + + Assert.assertFalse(line.contains(Line.fromPointAndAngle(p.add(offset2), Geometry.ZERO_PI, precision))); + Assert.assertFalse(line.contains(Line.fromPointAndAngle(p.subtract(offset2), Geometry.ZERO_PI, precision))); + + Assert.assertTrue(line.contains(Line.fromPointAndAngle(p, 1e-4, precision))); + Assert.assertFalse(line.contains(Line.fromPointAndAngle(p, 1e-2, precision))); + } + + @Test + public void testContains_point() { + // arrange + Vector2D p1 = Vector2D.of(-1, 0); + Vector2D p2 = Vector2D.of(0, 2); + Line line = Line.fromPoints(p1, p2, TEST_PRECISION); + + // act/assert + Assert.assertTrue(line.contains(p1)); + Assert.assertTrue(line.contains(p2)); + + Assert.assertFalse(line.contains(Vector2D.ZERO)); + Assert.assertFalse(line.contains(Vector2D.of(100, 79))); + + Vector2D offset1 = Vector2D.of(0.1, 0); + Vector2D offset2 = Vector2D.of(0, -0.1); + Vector2D v; + for (double t=-2; t<=2; t+=0.1) { + v = p1.lerp(p2, t); + + Assert.assertTrue(line.contains(v)); + + Assert.assertFalse(line.contains(v.add(offset1))); + Assert.assertFalse(line.contains(v.add(offset2))); + } + } + + @Test + public void testContains_point_closeToEpsilon() { + // arrange + double eps = 1e-3; + DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(eps); + + Vector2D p1 = Vector2D.of(-1, 0); + Vector2D p2 = Vector2D.of(0, 2); + Vector2D mid = p1.lerp(p2, 0.5); + + Line line = Line.fromPoints(p1, p2, precision); + Vector2D dir = line.getOffsetDirection(); + + // act/assert + Assert.assertTrue(line.contains(mid.add(dir.multiply(1e-4)))); + Assert.assertTrue(line.contains(mid.add(dir.multiply(-1e-4)))); + + Assert.assertFalse(line.contains(mid.add(dir.multiply(2e-3)))); + Assert.assertFalse(line.contains(mid.add(dir.multiply(-2e-3)))); + } + + @Test + public void testDistance_point() { + // arrange + Line line = Line.fromPoints(Vector2D.of(-1, 0), Vector2D.of(0, 2), TEST_PRECISION); + Line reversed = line.reverse(); + + // act/assert + Assert.assertEquals(0.0, line.distance(Vector2D.of(-0.5, 1)), TEST_EPS); + Assert.assertEquals(0.0, line.distance(Vector2D.of(-1.5, -1)), TEST_EPS); + Assert.assertEquals(0.0, line.distance(Vector2D.of(0.5, 3)), TEST_EPS); + + double d = Math.sin(Math.atan2(2, 1)); + + Assert.assertEquals(d, line.distance(Vector2D.ZERO), TEST_EPS); + Assert.assertEquals(d, line.distance(Vector2D.of(-1, 2)), TEST_EPS); + + Assert.assertEquals(d, reversed.distance(Vector2D.ZERO), TEST_EPS); + Assert.assertEquals(d, reversed.distance(Vector2D.of(-1, 2)), TEST_EPS); + } + + @Test + public void testDistance_point_permute() { + // arrange + Line line = Line.fromPoints(Vector2D.of(-1, 0), Vector2D.of(0, 2), TEST_PRECISION); + Vector2D lineOrigin = line.getOrigin(); + + EuclideanTestUtils.permute(-5, 5, 0.5, (x, y) -> { + Vector2D pt = Vector2D.of(x, y); + + // act + double dist = line.distance(pt); + + // arrange + Vector2D vec = lineOrigin.vectorTo(pt).reject(line.getDirection()); + Assert.assertEquals(vec.norm(), dist, TEST_EPS); + }); + } + + @Test + public void testIsParallel() { + // arrange + Vector2D dir = Vector2D.of(3, 7); + Line a = Line.fromPointAndDirection(Vector2D.of(1, 2), dir, TEST_PRECISION); + Line b = Line.fromPointAndDirection(Vector2D.of(0, -4), dir, TEST_PRECISION); + Line c = Line.fromPointAndDirection(Vector2D.of(-2, -2), dir.negate(), TEST_PRECISION); + Line d = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.PLUS_X, TEST_PRECISION); + + // act/assert + Assert.assertTrue(a.isParallel(a)); + + Assert.assertTrue(a.isParallel(b)); + Assert.assertTrue(b.isParallel(a)); + + Assert.assertTrue(a.isParallel(c)); + Assert.assertTrue(c.isParallel(a)); + + Assert.assertFalse(a.isParallel(d)); + Assert.assertFalse(d.isParallel(a)); } @Test - public void testParallel() { - Line l1 = new Line(Vector2D.of(0, 1), Vector2D.of(1, 2), TEST_PRECISION); - Line l2 = new Line(Vector2D.of(2, 2), Vector2D.of(3, 3), TEST_PRECISION); - Assert.assertTrue(l1.isParallelTo(l2)); - Line l3 = new Line(Vector2D.of(1, 0), Vector2D.of(0.5, -0.5), TEST_PRECISION); - Assert.assertTrue(l1.isParallelTo(l3)); - Line l4 = new Line(Vector2D.of(1, 0), Vector2D.of(0.5, -0.51), TEST_PRECISION); - Assert.assertTrue(! l1.isParallelTo(l4)); + public void testIsParallel_closeToParallel() { + // arrange + double eps = 1e-3; + DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(eps); + + Vector2D p1 = Vector2D.of(1, 2); + Vector2D p2 = Vector2D.of(1, -2); + + Line line = Line.fromPointAndAngle(p1, Geometry.ZERO_PI, precision); + + // act/assert + Assert.assertTrue(line.isParallel(Line.fromPointAndAngle(p2, 1e-4, precision))); + Assert.assertFalse(line.isParallel(Line.fromPointAndAngle(p2, 1e-2, precision))); } @Test public void testTransform() { + // arrange + AffineTransformMatrix2D scale = AffineTransformMatrix2D.createScale(2, 3); + AffineTransformMatrix2D reflect = AffineTransformMatrix2D.createScale(-1, 1); + AffineTransformMatrix2D translate = AffineTransformMatrix2D.createTranslation(3, 4); + AffineTransformMatrix2D rotate = AffineTransformMatrix2D.createRotation(Geometry.HALF_PI); + AffineTransformMatrix2D rotateAroundPt = AffineTransformMatrix2D.createRotation(Vector2D.of(0, 1), Geometry.HALF_PI); + + Vector2D p1 = Vector2D.of(0, 1); + Vector2D p2 = Vector2D.of(1, 0); + + Line horizontal = Line.fromPointAndDirection(p1, Vector2D.PLUS_X, TEST_PRECISION); + Line vertical = Line.fromPointAndDirection(p2, Vector2D.PLUS_Y, TEST_PRECISION); + Line diagonal = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION); + + // act/assert + Assert.assertSame(TEST_PRECISION, horizontal.transform(scale).getPrecision()); + + checkLine(horizontal.transform(scale), Vector2D.of(0, 3), Vector2D.PLUS_X); + checkLine(vertical.transform(scale), Vector2D.of(2, 0), Vector2D.PLUS_Y); + checkLine(diagonal.transform(scale), Vector2D.ZERO, Vector2D.of(2, 3).normalize()); - Line l1 = new Line(Vector2D.of(1.0 ,1.0), Vector2D.of(4.0 ,1.0), TEST_PRECISION); + checkLine(horizontal.transform(reflect), p1, Vector2D.MINUS_X); + checkLine(vertical.transform(reflect), Vector2D.of(-1, 0), Vector2D.PLUS_Y); + checkLine(diagonal.transform(reflect), Vector2D.ZERO, Vector2D.of(-1, 1).normalize()); + + checkLine(horizontal.transform(translate), Vector2D.of(0, 5), Vector2D.PLUS_X); + checkLine(vertical.transform(translate), Vector2D.of(4, 0), Vector2D.PLUS_Y); + checkLine(diagonal.transform(translate), Vector2D.of(-0.5, 0.5), Vector2D.of(1, 1).normalize()); + + checkLine(horizontal.transform(rotate), Vector2D.of(-1, 0), Vector2D.PLUS_Y); + checkLine(vertical.transform(rotate), Vector2D.of(0, 1), Vector2D.MINUS_X); + checkLine(diagonal.transform(rotate), Vector2D.ZERO, Vector2D.of(-1, 1).normalize()); + + checkLine(horizontal.transform(rotateAroundPt), Vector2D.ZERO, Vector2D.PLUS_Y); + checkLine(vertical.transform(rotateAroundPt), Vector2D.of(0, 2), Vector2D.MINUS_X); + checkLine(diagonal.transform(rotateAroundPt), Vector2D.of(1, 1), Vector2D.of(-1, 1).normalize()); + } + + @Test + public void testTransform_collapsedPoints() { + // arrange + AffineTransformMatrix2D scaleCollapse = AffineTransformMatrix2D.createScale(0, 1); + Line line = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.PLUS_X, TEST_PRECISION); + + // act/assert + GeometryTestUtils.assertThrows(() -> { + line.transform(scaleCollapse); + }, GeometryValueException.class, "Line direction cannot be zero"); + } + + @Test + public void testHashCode() { + // arrange + DoublePrecisionContext precision1 = new EpsilonDoublePrecisionContext(1e-4); + DoublePrecisionContext precision2 = new EpsilonDoublePrecisionContext(1e-5); + + Vector2D p = Vector2D.of(1, 2); + Vector2D v = Vector2D.of(1, 1); + + Line a = Line.fromPointAndDirection(p, v, precision1); + Line b = Line.fromPointAndDirection(Vector2D.ZERO, v, precision1); + Line c = Line.fromPointAndDirection(p, v.negate(), precision1); + Line d = Line.fromPointAndDirection(p, v, precision2); + Line e = Line.fromPointAndDirection(p, v, precision1); + + // act/assert + int aHash = a.hashCode(); + + Assert.assertEquals(aHash, a.hashCode()); + Assert.assertEquals(aHash, e.hashCode()); + + Assert.assertNotEquals(aHash, b.hashCode()); + Assert.assertNotEquals(aHash, c.hashCode()); + Assert.assertNotEquals(aHash, d.hashCode()); + } + + @Test + public void testEquals() { + // arrange + DoublePrecisionContext precision1 = new EpsilonDoublePrecisionContext(1e-4); + DoublePrecisionContext precision2 = new EpsilonDoublePrecisionContext(1e-5); + + Vector2D p = Vector2D.of(1, 2); + Vector2D v = Vector2D.of(1, 1); + + Line a = Line.fromPointAndDirection(p, v, precision1); + Line b = Line.fromPointAndDirection(Vector2D.ZERO, v, precision1); + Line c = Line.fromPointAndDirection(p, v.negate(), precision1); + Line d = Line.fromPointAndDirection(p, v, precision2); + Line e = Line.fromPointAndDirection(p, v, precision1); + + // act/assert + Assert.assertTrue(a.equals(a)); + Assert.assertTrue(a.equals(e)); + Assert.assertTrue(e.equals(a)); + + Assert.assertFalse(a.equals(null)); + Assert.assertFalse(a.equals(new Object())); + + Assert.assertFalse(a.equals(b)); + Assert.assertFalse(a.equals(c)); + Assert.assertFalse(a.equals(d)); + } + + @Test + public void testToString() { + // arrange + Line line = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.PLUS_X, TEST_PRECISION); + + // act + String str = line.toString(); + + // assert + Assert.assertTrue(str.contains("Line")); + Assert.assertTrue(str.contains("origin= (0.0, 0.0)")); + Assert.assertTrue(str.contains("direction= (1.0, 0.0)")); + } + + @Test + public void testLineTransform() { + + Line l1 = Line.fromPoints(Vector2D.of(1.0 ,1.0), Vector2D.of(4.0 ,1.0), TEST_PRECISION); Transform<Vector2D, Vector1D> t1 = - Line.getTransform(0.0, 0.5, -1.0, 0.0, 1.0, 1.5); + Line.getTransform(Vector2D.of(0.0, 0.5), Vector2D.of(-1.0, 0.0), Vector2D.of(1.0, 1.5)); Assert.assertEquals(0.5 * Math.PI, ((Line) t1.apply(l1)).getAngle(), 1.0e-10); - Line l2 = new Line(Vector2D.of(0.0, 0.0), Vector2D.of(1.0, 1.0), TEST_PRECISION); + Line l2 = Line.fromPoints(Vector2D.of(0.0, 0.0), Vector2D.of(1.0, 1.0), TEST_PRECISION); Transform<Vector2D, Vector1D> t2 = - Line.getTransform(0.0, 0.5, -1.0, 0.0, 1.0, 1.5); + Line.getTransform(Vector2D.of(0.0, 0.5), Vector2D.of(-1.0, 0.0), Vector2D.of(1.0, 1.5)); Assert.assertEquals(Math.atan2(1.0, -2.0), ((Line) t2.apply(l2)).getAngle(), 1.0e-10); } - @Test - public void testIntersection() { - Line l1 = new Line(Vector2D.of( 0, 1), Vector2D.of(1, 2), TEST_PRECISION); - Line l2 = new Line(Vector2D.of(-1, 2), Vector2D.of(2, 1), TEST_PRECISION); - Vector2D p = l1.intersection(l2); - Assert.assertEquals(0.5, p.getX(), 1.0e-10); - Assert.assertEquals(1.5, p.getY(), 1.0e-10); + /** + * Check that the line has the given defining properties. + * @param line + * @param origin + * @param dir + */ + private void checkLine(Line line, Vector2D origin, Vector2D dir) { + EuclideanTestUtils.assertCoordinatesEqual(origin, line.getOrigin(), TEST_EPS); + EuclideanTestUtils.assertCoordinatesEqual(dir, line.getDirection(), TEST_EPS); } - } diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolygonsSetTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolygonsSetTest.java index 14f917b..3ea43c5 100644 --- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolygonsSetTest.java +++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolygonsSetTest.java @@ -101,7 +101,7 @@ public class PolygonsSetTest { @Test public void testInfiniteLines_single() { // arrange - Line line = new Line(Vector2D.of(0, 0), Vector2D.of(1, 1), TEST_PRECISION); + Line line = Line.fromPoints(Vector2D.of(0, 0), Vector2D.of(1, 1), TEST_PRECISION); List<SubHyperplane<Vector2D>> boundaries = new ArrayList<SubHyperplane<Vector2D>>(); boundaries.add(line.wholeHyperplane()); @@ -137,8 +137,8 @@ public class PolygonsSetTest { @Test public void testInfiniteLines_twoIntersecting() { // arrange - Line line1 = new Line(Vector2D.of(0, 0), Vector2D.of(1, 1), TEST_PRECISION); - Line line2 = new Line(Vector2D.of(1, -1), Vector2D.of(0, 0), TEST_PRECISION); + Line line1 = Line.fromPoints(Vector2D.of(0, 0), Vector2D.of(1, 1), TEST_PRECISION); + Line line2 = Line.fromPoints(Vector2D.of(1, -1), Vector2D.of(0, 0), TEST_PRECISION); List<SubHyperplane<Vector2D>> boundaries = new ArrayList<SubHyperplane<Vector2D>>(); boundaries.add(line1.wholeHyperplane()); @@ -175,8 +175,8 @@ public class PolygonsSetTest { @Test public void testInfiniteLines_twoParallel_facingIn() { // arrange - Line line1 = new Line(Vector2D.of(1, 1), Vector2D.of(0, 1), TEST_PRECISION); - Line line2 = new Line(Vector2D.of(0, -1), Vector2D.of(1, -1), TEST_PRECISION); + Line line1 = Line.fromPoints(Vector2D.of(1, 1), Vector2D.of(0, 1), TEST_PRECISION); + Line line2 = Line.fromPoints(Vector2D.of(0, -1), Vector2D.of(1, -1), TEST_PRECISION); List<SubHyperplane<Vector2D>> boundaries = new ArrayList<SubHyperplane<Vector2D>>(); boundaries.add(line1.wholeHyperplane()); @@ -221,8 +221,8 @@ public class PolygonsSetTest { @Test public void testInfiniteLines_twoParallel_facingOut() { // arrange - Line line1 = new Line(Vector2D.of(0, 1), Vector2D.of(1, 1), TEST_PRECISION); - Line line2 = new Line(Vector2D.of(1, -1), Vector2D.of(0, -1), TEST_PRECISION); + Line line1 = Line.fromPoints(Vector2D.of(0, 1), Vector2D.of(1, 1), TEST_PRECISION); + Line line2 = Line.fromPoints(Vector2D.of(1, -1), Vector2D.of(0, -1), TEST_PRECISION); List<SubHyperplane<Vector2D>> boundaries = new ArrayList<SubHyperplane<Vector2D>>(); boundaries.add(line1.wholeHyperplane()); @@ -267,8 +267,8 @@ public class PolygonsSetTest { @Test public void testMixedFiniteAndInfiniteLines_explicitInfiniteBoundaries() { // arrange - Line line1 = new Line(Vector2D.of(3, 3), Vector2D.of(0, 3), TEST_PRECISION); - Line line2 = new Line(Vector2D.of(0, -3), Vector2D.of(3, -3), TEST_PRECISION); + Line line1 = Line.fromPoints(Vector2D.of(3, 3), Vector2D.of(0, 3), TEST_PRECISION); + Line line2 = Line.fromPoints(Vector2D.of(0, -3), Vector2D.of(3, -3), TEST_PRECISION); List<SubHyperplane<Vector2D>> boundaries = new ArrayList<SubHyperplane<Vector2D>>(); boundaries.add(line1.wholeHyperplane()); @@ -318,7 +318,7 @@ public class PolygonsSetTest { @Test public void testMixedFiniteAndInfiniteLines_impliedInfiniteBoundaries() { // arrange - Line line = new Line(Vector2D.of(3, 0), Vector2D.of(3, 3), TEST_PRECISION); + Line line = Line.fromPoints(Vector2D.of(3, 0), Vector2D.of(3, 3), TEST_PRECISION); List<SubHyperplane<Vector2D>> boundaries = new ArrayList<SubHyperplane<Vector2D>>(); boundaries.add(buildSegment(Vector2D.of(0, 3), Vector2D.of(0, 0))); @@ -714,7 +714,7 @@ public class PolygonsSetTest { PolygonsSet set = buildSet(vertices); // assert - Line l1 = new Line(Vector2D.of(-1.5, 0.0), Math.PI / 4, TEST_PRECISION); + Line l1 = Line.fromPointAndAngle(Vector2D.of(-1.5, 0.0), Math.PI / 4, TEST_PRECISION); SubLine s1 = (SubLine) set.intersection(l1.wholeHyperplane()); List<Interval> i1 = ((IntervalsSet) s1.getRemainingRegion()).asList(); Assert.assertEquals(2, i1.size()); @@ -733,7 +733,7 @@ public class PolygonsSetTest { Assert.assertEquals(1.5, p11Upper.getX(), TEST_EPS); Assert.assertEquals(3.0, p11Upper.getY(), TEST_EPS); - Line l2 = new Line(Vector2D.of(-1.0, 2.0), 0, TEST_PRECISION); + Line l2 = Line.fromPointAndAngle(Vector2D.of(-1.0, 2.0), 0, TEST_PRECISION); SubLine s2 = (SubLine) set.intersection(l2.wholeHyperplane()); List<Interval> i2 = ((IntervalsSet) s2.getRemainingRegion()).asList(); Assert.assertEquals(1, i2.size()); @@ -1094,13 +1094,13 @@ public class PolygonsSetTest { double pi6 = Math.PI / 6.0; double sqrt3 = Math.sqrt(3.0); SubLine[] hyp = { - new Line(Vector2D.of( 0.0, 1.0), 5 * pi6, TEST_PRECISION).wholeHyperplane(), - new Line(Vector2D.of(-sqrt3, 1.0), 7 * pi6, TEST_PRECISION).wholeHyperplane(), - new Line(Vector2D.of(-sqrt3, 1.0), 9 * pi6, TEST_PRECISION).wholeHyperplane(), - new Line(Vector2D.of(-sqrt3, 0.0), 11 * pi6, TEST_PRECISION).wholeHyperplane(), - new Line(Vector2D.of( 0.0, 0.0), 13 * pi6, TEST_PRECISION).wholeHyperplane(), - new Line(Vector2D.of( 0.0, 1.0), 3 * pi6, TEST_PRECISION).wholeHyperplane(), - new Line(Vector2D.of(-5.0 * sqrt3 / 6.0, 0.0), 9 * pi6, TEST_PRECISION).wholeHyperplane() + Line.fromPointAndAngle(Vector2D.of( 0.0, 1.0), 5 * pi6, TEST_PRECISION).wholeHyperplane(), + Line.fromPointAndAngle(Vector2D.of(-sqrt3, 1.0), 7 * pi6, TEST_PRECISION).wholeHyperplane(), + Line.fromPointAndAngle(Vector2D.of(-sqrt3, 1.0), 9 * pi6, TEST_PRECISION).wholeHyperplane(), + Line.fromPointAndAngle(Vector2D.of(-sqrt3, 0.0), 11 * pi6, TEST_PRECISION).wholeHyperplane(), + Line.fromPointAndAngle(Vector2D.of( 0.0, 0.0), 13 * pi6, TEST_PRECISION).wholeHyperplane(), + Line.fromPointAndAngle(Vector2D.of( 0.0, 1.0), 3 * pi6, TEST_PRECISION).wholeHyperplane(), + Line.fromPointAndAngle(Vector2D.of(-5.0 * sqrt3 / 6.0, 0.0), 9 * pi6, TEST_PRECISION).wholeHyperplane() }; hyp[1] = (SubLine) hyp[1].split(hyp[0].getHyperplane()).getMinus(); hyp[2] = (SubLine) hyp[2].split(hyp[1].getHyperplane()).getMinus(); @@ -1114,7 +1114,7 @@ public class PolygonsSetTest { } PolygonsSet set = new PolygonsSet(tree, TEST_PRECISION); SubLine splitter = - new Line(Vector2D.of(-2.0 * sqrt3 / 3.0, 0.0), 9 * pi6, TEST_PRECISION).wholeHyperplane(); + Line.fromPointAndAngle(Vector2D.of(-2.0 * sqrt3 / 3.0, 0.0), 9 * pi6, TEST_PRECISION).wholeHyperplane(); // act PolygonsSet slice = @@ -1295,13 +1295,13 @@ public class PolygonsSetTest { public void testBug20041003() { // arrange Line[] l = { - new Line(Vector2D.of(0.0, 0.625000007541172), + Line.fromPoints(Vector2D.of(0.0, 0.625000007541172), Vector2D.of(1.0, 0.625000007541172), TEST_PRECISION), - new Line(Vector2D.of(-0.19204433621902645, 0.0), + Line.fromPoints(Vector2D.of(-0.19204433621902645, 0.0), Vector2D.of(-0.19204433621902645, 1.0), TEST_PRECISION), - new Line(Vector2D.of(-0.40303524786887, 0.4248364535319128), + Line.fromPoints(Vector2D.of(-0.40303524786887, 0.4248364535319128), Vector2D.of(-1.12851149797877, -0.2634107480798909), TEST_PRECISION), - new Line(Vector2D.of(0.0, 2.0), + Line.fromPoints(Vector2D.of(0.0, 2.0), Vector2D.of(1.0, 2.0), TEST_PRECISION) }; @@ -1588,10 +1588,10 @@ public class PolygonsSetTest { // if tolerance is smaller than rectangle width, the rectangle is computed accurately DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1.0 / 256); Hyperplane<Vector2D>[] h1 = new Line[] { - new Line(pA, pB, precision), - new Line(pB, pC, precision), - new Line(pC, pD, precision), - new Line(pD, pA, precision) + Line.fromPoints(pA, pB, precision), + Line.fromPoints(pB, pC, precision), + Line.fromPoints(pC, pD, precision), + Line.fromPoints(pD, pA, precision) }; // act @@ -1619,10 +1619,10 @@ public class PolygonsSetTest { DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1.0 / 16); Hyperplane<Vector2D>[] h2 = new Line[] { - new Line(pA, pB, precision), - new Line(pB, pC, precision), - new Line(pC, pD, precision), - new Line(pD, pA, precision) + Line.fromPointAndDirection(pA, Vector2D.MINUS_Y, precision), + Line.fromPointAndDirection(pB, Vector2D.PLUS_X, precision), + Line.fromPointAndDirection(pC, Vector2D.PLUS_Y, precision), + Line.fromPointAndDirection(pD, Vector2D.MINUS_X, precision) }; // act @@ -1636,8 +1636,8 @@ public class PolygonsSetTest { @Test(expected = IllegalArgumentException.class) public void testInconsistentHyperplanes() { // act - new RegionFactory<Vector2D>().buildConvex(new Line(Vector2D.of(0, 0), Vector2D.of(0, 1), TEST_PRECISION), - new Line(Vector2D.of(1, 1), Vector2D.of(1, 0), TEST_PRECISION)); + new RegionFactory<Vector2D>().buildConvex(Line.fromPoints(Vector2D.of(0, 0), Vector2D.of(0, 1), TEST_PRECISION), + Line.fromPoints(Vector2D.of(1, 1), Vector2D.of(1, 0), TEST_PRECISION)); } @Test @@ -1658,7 +1658,7 @@ public class PolygonsSetTest { // splitting the square in two halves increases the BSP tree // with 3 more cuts and 3 more leaf nodes - SubLine cut = new Line(Vector2D.of(0.5, 0.5), 0.0, square.getPrecision()).wholeHyperplane(); + SubLine cut = Line.fromPointAndAngle(Vector2D.of(0.5, 0.5), 0.0, square.getPrecision()).wholeHyperplane(); PolygonsSet splitSquare = new PolygonsSet(square.getTree(false).split(cut), square.getPrecision()); Counter splitSquareCount = new Counter(); @@ -1720,7 +1720,7 @@ public class PolygonsSetTest { } private SubHyperplane<Vector2D> buildLine(Vector2D start, Vector2D end) { - return new Line(start, end, TEST_PRECISION).wholeHyperplane(); + return Line.fromPoints(start, end, TEST_PRECISION).wholeHyperplane(); } private double intersectionAbscissa(Line l0, Line l1) { @@ -1730,14 +1730,14 @@ public class PolygonsSetTest { private SubHyperplane<Vector2D> buildHalfLine(Vector2D start, Vector2D end, boolean startIsVirtual) { - Line line = new Line(start, end, TEST_PRECISION); + Line line = Line.fromPoints(start, end, TEST_PRECISION); double lower = startIsVirtual ? Double.NEGATIVE_INFINITY : (line.toSubSpace(start)).getX(); double upper = startIsVirtual ? (line.toSubSpace(end)).getX() : Double.POSITIVE_INFINITY; return new SubLine(line, new IntervalsSet(lower, upper, TEST_PRECISION)); } private SubHyperplane<Vector2D> buildSegment(Vector2D start, Vector2D end) { - Line line = new Line(start, end, TEST_PRECISION); + Line line = Line.fromPoints(start, end, TEST_PRECISION); double lower = (line.toSubSpace(start)).getX(); double upper = (line.toSubSpace(end)).getX(); return new SubLine(line, new IntervalsSet(lower, upper, TEST_PRECISION)); diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/SegmentTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/SegmentTest.java index cd94cfe..d6085b6 100644 --- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/SegmentTest.java +++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/SegmentTest.java @@ -32,7 +32,7 @@ public class SegmentTest { public void testDistance() { Vector2D start = Vector2D.of(2, 2); Vector2D end = Vector2D.of(-2, -2); - Segment segment = new Segment(start, end, new Line(start, end, TEST_PRECISION)); + Segment segment = new Segment(start, end, Line.fromPoints(start, end, TEST_PRECISION)); // distance to center of segment Assert.assertEquals(Math.sqrt(2), segment.distance(Vector2D.of(1, -1)), TEST_EPS); diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/SubLineTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/SubLineTest.java index 6ef6638..2e086bc 100644 --- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/SubLineTest.java +++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/SubLineTest.java @@ -37,7 +37,7 @@ public class SubLineTest { public void testEndPoints() { Vector2D p1 = Vector2D.of(-1, -7); Vector2D p2 = Vector2D.of(7, -1); - Segment segment = new Segment(p1, p2, new Line(p1, p2, TEST_PRECISION)); + Segment segment = new Segment(p1, p2, Line.fromPoints(p1, p2, TEST_PRECISION)); SubLine sub = new SubLine(segment); List<Segment> segments = sub.getSegments(); Assert.assertEquals(1, segments.size()); @@ -47,7 +47,7 @@ public class SubLineTest { @Test public void testNoEndPoints() { - SubLine wholeLine = new Line(Vector2D.of(-1, 7), Vector2D.of(7, 1), TEST_PRECISION).wholeHyperplane(); + SubLine wholeLine = Line.fromPoints(Vector2D.of(-1, 7), Vector2D.of(7, 1), TEST_PRECISION).wholeHyperplane(); List<Segment> segments = wholeLine.getSegments(); Assert.assertEquals(1, segments.size()); Assert.assertTrue(Double.isInfinite(segments.get(0).getStart().getX()) && @@ -62,7 +62,7 @@ public class SubLineTest { @Test public void testNoSegments() { - SubLine empty = new SubLine(new Line(Vector2D.of(-1, -7), Vector2D.of(7, -1), TEST_PRECISION), + SubLine empty = new SubLine(Line.fromPoints(Vector2D.of(-1, -7), Vector2D.of(7, -1), TEST_PRECISION), new RegionFactory<Vector1D>().getComplement(new IntervalsSet(TEST_PRECISION))); List<Segment> segments = empty.getSegments(); Assert.assertEquals(0, segments.size()); @@ -70,7 +70,7 @@ public class SubLineTest { @Test public void testSeveralSegments() { - SubLine twoSubs = new SubLine(new Line(Vector2D.of(-1, -7), Vector2D.of(7, -1), TEST_PRECISION), + SubLine twoSubs = new SubLine(Line.fromPoints(Vector2D.of(-1, -7), Vector2D.of(7, -1), TEST_PRECISION), new RegionFactory<Vector1D>().union(new IntervalsSet(1, 2, TEST_PRECISION), new IntervalsSet(3, 4, TEST_PRECISION))); List<Segment> segments = twoSubs.getSegments(); @@ -79,7 +79,7 @@ public class SubLineTest { @Test public void testHalfInfiniteNeg() { - SubLine empty = new SubLine(new Line(Vector2D.of(-1, -7), Vector2D.of(7, -1), TEST_PRECISION), + SubLine empty = new SubLine(Line.fromPoints(Vector2D.of(-1, -7), Vector2D.of(7, -1), TEST_PRECISION), new IntervalsSet(Double.NEGATIVE_INFINITY, 0.0, TEST_PRECISION)); List<Segment> segments = empty.getSegments(); Assert.assertEquals(1, segments.size()); @@ -92,7 +92,7 @@ public class SubLineTest { @Test public void testHalfInfinitePos() { - SubLine empty = new SubLine(new Line(Vector2D.of(-1, -7), Vector2D.of(7, -1), TEST_PRECISION), + SubLine empty = new SubLine(Line.fromPoints(Vector2D.of(-1, -7), Vector2D.of(7, -1), TEST_PRECISION), new IntervalsSet(0.0, Double.POSITIVE_INFINITY, TEST_PRECISION)); List<Segment> segments = empty.getSegments(); Assert.assertEquals(1, segments.size()); diff --git a/commons-geometry-hull/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/ConvexHull2D.java b/commons-geometry-hull/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/ConvexHull2D.java index 2aee11d..7dc7b26 100644 --- a/commons-geometry-hull/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/ConvexHull2D.java +++ b/commons-geometry-hull/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/ConvexHull2D.java @@ -127,7 +127,7 @@ public class ConvexHull2D implements ConvexHull<Vector2D>, Serializable { this.lineSegments = new Segment[1]; final Vector2D p1 = vertices[0]; final Vector2D p2 = vertices[1]; - this.lineSegments[0] = new Segment(p1, p2, new Line(p1, p2, precision)); + this.lineSegments[0] = new Segment(p1, p2, Line.fromPoints(p1, p2, precision)); } else { this.lineSegments = new Segment[size]; Vector2D firstPoint = null; @@ -139,12 +139,12 @@ public class ConvexHull2D implements ConvexHull<Vector2D>, Serializable { lastPoint = point; } else { this.lineSegments[index++] = - new Segment(lastPoint, point, new Line(lastPoint, point, precision)); + new Segment(lastPoint, point, Line.fromPoints(lastPoint, point, precision)); lastPoint = point; } } this.lineSegments[index] = - new Segment(lastPoint, firstPoint, new Line(lastPoint, firstPoint, precision)); + new Segment(lastPoint, firstPoint, Line.fromPoints(lastPoint, firstPoint, precision)); } } return lineSegments; diff --git a/commons-geometry-hull/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/MonotoneChain.java b/commons-geometry-hull/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/MonotoneChain.java index 80a1686..98d019d 100644 --- a/commons-geometry-hull/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/MonotoneChain.java +++ b/commons-geometry-hull/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/MonotoneChain.java @@ -147,7 +147,7 @@ public class MonotoneChain extends AbstractConvexHullGenerator2D { final Vector2D p1 = hull.get(size - 2); final Vector2D p2 = hull.get(size - 1); - final double offset = new Line(p1, p2, precision).getOffset(point); + final double offset = Line.fromPoints(p1, p2, precision).getOffset(point); if (precision.eqZero(offset)) { // the point is collinear to the line (p1, p2) diff --git a/commons-geometry-hull/src/test/java/org/apache/commons/geometry/euclidean/twod/hull/MonotoneChainTest.java b/commons-geometry-hull/src/test/java/org/apache/commons/geometry/euclidean/twod/hull/MonotoneChainTest.java index 2a24af4..2b91053 100644 --- a/commons-geometry-hull/src/test/java/org/apache/commons/geometry/euclidean/twod/hull/MonotoneChainTest.java +++ b/commons-geometry-hull/src/test/java/org/apache/commons/geometry/euclidean/twod/hull/MonotoneChainTest.java @@ -49,7 +49,7 @@ public class MonotoneChainTest extends ConvexHullGenerator2DAbstractTest { points.add(Vector2D.of(40, 1)); @SuppressWarnings("unused") - final ConvexHull2D hull = new MonotoneChain(true, new EpsilonDoublePrecisionContext(2)).generate(points); + final ConvexHull2D hull = new MonotoneChain(true, new EpsilonDoublePrecisionContext(1)).generate(points); } }