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);
     }
 
 }

Reply via email to