LineTest.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.geometry.euclidean.twod;

import org.apache.commons.geometry.core.GeometryTestUtils;
import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
import org.apache.commons.geometry.euclidean.oned.AffineTransformMatrix1D;
import org.apache.commons.geometry.euclidean.oned.Vector1D;
import org.apache.commons.geometry.euclidean.twod.Line.SubspaceTransform;
import org.apache.commons.numbers.angle.Angle;
import org.apache.commons.numbers.core.Precision;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

class LineTest {

    private static final double TEST_EPS = 1e-10;

    private static final Precision.DoubleEquivalence TEST_PRECISION =
            Precision.doubleEquivalenceOfEpsilon(TEST_EPS);

    @Test
    void testFromPoints() {
        // act/assert
        checkLine(Lines.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION),
                Vector2D.ZERO, Vector2D.Unit.PLUS_X);
        checkLine(Lines.fromPoints(Vector2D.ZERO, Vector2D.of(100, 0), TEST_PRECISION),
                Vector2D.ZERO, Vector2D.Unit.PLUS_X);
        checkLine(Lines.fromPoints(Vector2D.of(100, 0), Vector2D.ZERO, TEST_PRECISION),
                Vector2D.ZERO, Vector2D.Unit.MINUS_X);
        checkLine(Lines.fromPoints(Vector2D.of(-100, 0), Vector2D.of(100, 0), TEST_PRECISION),
                Vector2D.ZERO, Vector2D.Unit.PLUS_X);

        checkLine(Lines.fromPoints(Vector2D.of(-2, 0), Vector2D.of(0, 2), TEST_PRECISION),
                Vector2D.of(-1, 1), Vector2D.of(1, 1).normalize());
        checkLine(Lines.fromPoints(Vector2D.of(0, 2), Vector2D.of(-2, 0), TEST_PRECISION),
                Vector2D.of(-1, 1), Vector2D.of(-1, -1).normalize());
    }

    @Test
    void testFromPoints_pointsTooClose() {
        // act/assert
        GeometryTestUtils.assertThrowsWithMessage(() -> Lines.fromPoints(Vector2D.Unit.PLUS_X, Vector2D.Unit.PLUS_X, TEST_PRECISION),
                IllegalArgumentException.class, "Line direction cannot be zero");
        GeometryTestUtils.assertThrowsWithMessage(() -> Lines.fromPoints(Vector2D.Unit.PLUS_X, Vector2D.of(1 + 1e-11, 1e-11), TEST_PRECISION),
                IllegalArgumentException.class, "Line direction cannot be zero");
    }

    @Test
    void testFromPointAndDirection() {
        // act/assert
        checkLine(Lines.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION),
                Vector2D.ZERO, Vector2D.Unit.PLUS_X);
        checkLine(Lines.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(100, 0), TEST_PRECISION),
                Vector2D.ZERO, Vector2D.Unit.PLUS_X);
        checkLine(Lines.fromPointAndDirection(Vector2D.of(-100, 0), Vector2D.of(100, 0), TEST_PRECISION),
                Vector2D.ZERO, Vector2D.Unit.PLUS_X);

        checkLine(Lines.fromPointAndDirection(Vector2D.of(-2, 0), Vector2D.of(1, 1), TEST_PRECISION),
                Vector2D.of(-1, 1), Vector2D.of(1, 1).normalize());
        checkLine(Lines.fromPointAndDirection(Vector2D.of(0, 2), Vector2D.of(-1, -1), TEST_PRECISION),
                Vector2D.of(-1, 1), Vector2D.of(-1, -1).normalize());
    }

    @Test
    void testFromPointAndDirection_directionIsZero() {
        // act/assert
        GeometryTestUtils.assertThrowsWithMessage(() -> Lines.fromPointAndDirection(Vector2D.Unit.PLUS_X, Vector2D.ZERO, TEST_PRECISION),
                IllegalArgumentException.class, "Line direction cannot be zero");
        GeometryTestUtils.assertThrowsWithMessage(() -> Lines.fromPointAndDirection(Vector2D.Unit.PLUS_X, Vector2D.of(1e-11, -1e-12), TEST_PRECISION),
                IllegalArgumentException.class, "Line direction cannot be zero");
    }

    @Test
    void testFromPointAndAngle() {
        // act/assert
        checkLine(Lines.fromPointAndAngle(Vector2D.ZERO, 0, TEST_PRECISION),
                Vector2D.ZERO, Vector2D.Unit.PLUS_X);
        checkLine(Lines.fromPointAndAngle(Vector2D.of(1, 1), Angle.PI_OVER_TWO, TEST_PRECISION),
                Vector2D.of(1, 0), Vector2D.Unit.PLUS_Y);
        checkLine(Lines.fromPointAndAngle(Vector2D.of(-1, -1), Math.PI, TEST_PRECISION),
                Vector2D.of(0, -1), Vector2D.Unit.MINUS_X);
        checkLine(Lines.fromPointAndAngle(Vector2D.of(1, -1), -Angle.PI_OVER_TWO, TEST_PRECISION),
                Vector2D.of(1, 0), Vector2D.Unit.MINUS_Y);
        checkLine(Lines.fromPointAndAngle(Vector2D.of(-1, 1), Angle.TWO_PI, TEST_PRECISION),
                Vector2D.of(0, 1), Vector2D.Unit.PLUS_X);
    }

    @Test
    void testGetAngle() {
        // arrange
        final Vector2D vec = Vector2D.of(1, 2);

        for (double theta = -4 * Math.PI; theta < 2 * Math.PI; theta += 0.1) {
            final Line line = Lines.fromPointAndAngle(vec, theta, TEST_PRECISION);

            // act/assert
            Assertions.assertEquals(Angle.Rad.WITHIN_0_AND_2PI.applyAsDouble(theta),
                    line.getAngle(), TEST_EPS);
        }
    }

    @Test
    void testGetAngle_multiplesOfPi() {
        // arrange
        final Vector2D vec = Vector2D.of(-1, -2);

        // act/assert
        Assertions.assertEquals(0, Lines.fromPointAndAngle(vec, 0.0, TEST_PRECISION).getAngle(), TEST_EPS);
        Assertions.assertEquals(Math.PI, Lines.fromPointAndAngle(vec, Math.PI, TEST_PRECISION).getAngle(), TEST_EPS);
        Assertions.assertEquals(0, Lines.fromPointAndAngle(vec, Angle.TWO_PI, TEST_PRECISION).getAngle(), TEST_EPS);

        Assertions.assertEquals(0, Lines.fromPointAndAngle(vec, -2 * Math.PI, TEST_PRECISION).getAngle(), TEST_EPS);
        Assertions.assertEquals(Math.PI, Lines.fromPointAndAngle(vec, -3 * Math.PI, TEST_PRECISION).getAngle(), TEST_EPS);
        Assertions.assertEquals(0, Lines.fromPointAndAngle(vec, -4 * Angle.TWO_PI, TEST_PRECISION).getAngle(), TEST_EPS);
    }

    @Test
    void testGetDirection() {
        // act/assert
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.PLUS_X,
                Lines.fromPoints(Vector2D.of(0, 0), Vector2D.of(1, 0), TEST_PRECISION).getDirection(), TEST_EPS);
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.MINUS_Y,
                Lines.fromPoints(Vector2D.of(0, 1), Vector2D.of(0, -1), TEST_PRECISION).getDirection(), TEST_EPS);

        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.MINUS_X,
                Lines.fromPoints(Vector2D.of(2, 2), Vector2D.of(1, 2), TEST_PRECISION).getDirection(), TEST_EPS);
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.PLUS_X,
                Lines.fromPoints(Vector2D.of(10, -2), Vector2D.of(10.1, -2), TEST_PRECISION).getDirection(), TEST_EPS);

        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.MINUS_Y,
                Lines.fromPoints(Vector2D.of(3, 2), Vector2D.of(3, 1), TEST_PRECISION).getDirection(), TEST_EPS);
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.PLUS_Y,
                Lines.fromPoints(Vector2D.of(-3, 10), Vector2D.of(-3, 10.1), TEST_PRECISION).getDirection(), TEST_EPS);

        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, -1).normalize(),
                Lines.fromPoints(Vector2D.of(0, 2), Vector2D.of(2, 0), TEST_PRECISION).getDirection(), TEST_EPS);
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, 1).normalize(),
                Lines.fromPoints(Vector2D.of(2, 0), Vector2D.of(0, 2), TEST_PRECISION).getDirection(), TEST_EPS);
    }

    @Test
    void testGetOffsetDirection() {
        // act/assert
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.MINUS_Y,
                Lines.fromPoints(Vector2D.of(0, 0), Vector2D.of(1, 0), TEST_PRECISION).getOffsetDirection(), TEST_EPS);
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.MINUS_X,
                Lines.fromPoints(Vector2D.of(0, 1), Vector2D.of(0, -1), TEST_PRECISION).getOffsetDirection(), TEST_EPS);

        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.PLUS_Y,
                Lines.fromPoints(Vector2D.of(2, 2), Vector2D.of(1, 2), TEST_PRECISION).getOffsetDirection(), TEST_EPS);
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.MINUS_Y,
                Lines.fromPoints(Vector2D.of(10, -2), Vector2D.of(10.1, -2), TEST_PRECISION).getOffsetDirection(), TEST_EPS);

        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.MINUS_X,
                Lines.fromPoints(Vector2D.of(3, 2), Vector2D.of(3, 1), TEST_PRECISION).getOffsetDirection(), TEST_EPS);
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.PLUS_X,
                Lines.fromPoints(Vector2D.of(-3, 10), Vector2D.of(-3, 10.1), TEST_PRECISION).getOffsetDirection(), TEST_EPS);

        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, -1).normalize(),
                Lines.fromPoints(Vector2D.of(0, 2), Vector2D.of(2, 0), TEST_PRECISION).getOffsetDirection(), TEST_EPS);
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1).normalize(),
                Lines.fromPoints(Vector2D.of(2, 0), Vector2D.of(0, 2), TEST_PRECISION).getOffsetDirection(), TEST_EPS);
    }

    @Test
    void testGetOrigin() {
        // act/assert
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO,
                Lines.fromPoints(Vector2D.of(0, 0), Vector2D.of(1, 0), TEST_PRECISION).getOrigin(), TEST_EPS);
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO,
                Lines.fromPoints(Vector2D.of(0, 1), Vector2D.of(0, -1), TEST_PRECISION).getOrigin(), TEST_EPS);

        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, 2),
                Lines.fromPoints(Vector2D.of(2, 2), Vector2D.of(3, 2), TEST_PRECISION).getOrigin(), TEST_EPS);
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, -2),
                Lines.fromPoints(Vector2D.of(10, -2), Vector2D.of(10.1, -2), TEST_PRECISION).getOrigin(), TEST_EPS);

        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(3, 0),
                Lines.fromPoints(Vector2D.of(3, 2), Vector2D.of(3, 1), TEST_PRECISION).getOrigin(), TEST_EPS);
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-3, 0),
                Lines.fromPoints(Vector2D.of(-3, 10), Vector2D.of(-3, 10.1), TEST_PRECISION).getOrigin(), TEST_EPS);

        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1),
                Lines.fromPoints(Vector2D.of(0, 2), Vector2D.of(2, 0), TEST_PRECISION).getOrigin(), TEST_EPS);
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1),
                Lines.fromPoints(Vector2D.of(2, 0), Vector2D.of(0, 2), TEST_PRECISION).getOrigin(), TEST_EPS);
    }

    @Test
    void testGetOriginOffset() {
        // arrange
        final double sqrt2 = Math.sqrt(2);

        // act/assert
        Assertions.assertEquals(0.0,
                Lines.fromPoints(Vector2D.of(0, 0), Vector2D.of(1, 1), TEST_PRECISION).getOriginOffset(), TEST_EPS);
        Assertions.assertEquals(0.0,
                Lines.fromPoints(Vector2D.of(0, 0), Vector2D.of(-1, -1), TEST_PRECISION).getOriginOffset(), TEST_EPS);

        Assertions.assertEquals(sqrt2,
                Lines.fromPoints(Vector2D.of(-1, 1), Vector2D.of(0, 2), TEST_PRECISION).getOriginOffset(), TEST_EPS);
        Assertions.assertEquals(-sqrt2,
                Lines.fromPoints(Vector2D.of(0, -2), Vector2D.of(1, -1), TEST_PRECISION).getOriginOffset(), TEST_EPS);

        Assertions.assertEquals(-sqrt2,
                Lines.fromPoints(Vector2D.of(0, 2), Vector2D.of(-1, 1), TEST_PRECISION).getOriginOffset(), TEST_EPS);
        Assertions.assertEquals(sqrt2,
                Lines.fromPoints(Vector2D.of(1, -1), Vector2D.of(0, -2), TEST_PRECISION).getOriginOffset(), TEST_EPS);
    }

    @Test
    void testGetPrecision() {
        // act/assert
        Assertions.assertSame(TEST_PRECISION, Lines.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION).getPrecision());
        Assertions.assertSame(TEST_PRECISION, Lines.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION).getPrecision());
        Assertions.assertSame(TEST_PRECISION, Lines.fromPointAndAngle(Vector2D.ZERO, 0, TEST_PRECISION).getPrecision());
    }

    @Test
    void testReverse() {
        // arrange
        final Vector2D pt = Vector2D.of(0, 1);
        final Vector2D dir = Vector2D.Unit.PLUS_X;
        final Line line = Lines.fromPointAndDirection(pt, dir, TEST_PRECISION);

        // act
        final Line reversed = line.reverse();
        final Line doubleReversed = reversed.reverse();

        // assert
        checkLine(reversed, pt, dir.negate());
        Assertions.assertEquals(-1, reversed.getOriginOffset(), TEST_EPS);

        checkLine(doubleReversed, pt, dir);
        Assertions.assertEquals(1, doubleReversed.getOriginOffset(), TEST_EPS);
    }

    @Test
    void testAbscissa() {
        // arrange
        final Line line = Lines.fromPoints(Vector2D.of(-2, -2), Vector2D.of(2, 1), TEST_PRECISION);

        // act/assert
        Assertions.assertEquals(0.0, line.abscissa(Vector2D.of(-3, 4)), TEST_EPS);
        Assertions.assertEquals(0.0, line.abscissa(Vector2D.of(3, -4)), TEST_EPS);
        Assertions.assertEquals(5.0, line.abscissa(Vector2D.of(7, -1)), TEST_EPS);
        Assertions.assertEquals(-5.0, line.abscissa(Vector2D.of(-1, -7)), TEST_EPS);
    }

    @Test
    void testToSubspace() {
        // arrange
        final Line line = Lines.fromPoints(Vector2D.of(2, 1), Vector2D.of(-2, -2), TEST_PRECISION);

        // act/assert
        Assertions.assertEquals(0.0, line.toSubspace(Vector2D.of(-3, 4)).getX(), TEST_EPS);
        Assertions.assertEquals(0.0, line.toSubspace(Vector2D.of(3, -4)).getX(), TEST_EPS);
        Assertions.assertEquals(-5.0, line.toSubspace(Vector2D.of(7, -1)).getX(), TEST_EPS);
        Assertions.assertEquals(5.0, line.toSubspace(Vector2D.of(-1, -7)).getX(), TEST_EPS);
    }

    @Test
    void testToSpace_throughOrigin() {
        // arrange
        final double invSqrt2 = 1 / Math.sqrt(2);
        final Vector2D dir = Vector2D.of(invSqrt2, invSqrt2);

        final Line line = Lines.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
    void testToSpace_offsetFromOrigin() {
        // arrange
        final double angle = Math.PI / 6;
        final double cos = Math.cos(angle);
        final double sin = Math.sin(angle);
        final Vector2D pt = Vector2D.of(-5, 0);

        final double h = Math.abs(pt.getX()) * cos;
        final double d = h * cos;
        final Vector2D origin = Vector2D.of(
                    pt.getX() + d,
                    h * sin
                );
        final Vector2D dir = Vector2D.of(cos, sin);

        final Line line = Lines.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
    void testIntersection() {
        // arrange
        final Line a = Lines.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
        final Line b = Lines.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_Y, TEST_PRECISION);
        final Line c = Lines.fromPointAndDirection(Vector2D.of(0, 2), Vector2D.of(2, 1), TEST_PRECISION);
        final Line d = Lines.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
    void testIntersection_parallel() {
        // arrange
        final Line a = Lines.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
        final Line b = Lines.fromPointAndDirection(Vector2D.of(0, 1), Vector2D.Unit.PLUS_X, TEST_PRECISION);

        final Line c = Lines.fromPointAndDirection(Vector2D.of(0, 2), Vector2D.of(2, 1), TEST_PRECISION);
        final Line d = Lines.fromPointAndDirection(Vector2D.of(0, -1), Vector2D.of(2, 1), TEST_PRECISION);

        // act/assert
        Assertions.assertNull(a.intersection(b));
        Assertions.assertNull(b.intersection(a));

        Assertions.assertNull(c.intersection(d));
        Assertions.assertNull(d.intersection(c));
    }

    @Test
    void testIntersection_coincident() {
        // arrange
        final Line a = Lines.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
        final Line b = Lines.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);

        final Line c = Lines.fromPointAndDirection(Vector2D.of(0, 2), Vector2D.of(2, 1), TEST_PRECISION);
        final Line d = Lines.fromPointAndDirection(Vector2D.of(0, 2), Vector2D.of(2, 1), TEST_PRECISION);

        // act/assert
        Assertions.assertNull(a.intersection(b));
        Assertions.assertNull(b.intersection(a));

        Assertions.assertNull(c.intersection(d));
        Assertions.assertNull(d.intersection(c));
    }

    @Test
    void testAngle() {
        // arrange
        final Line a = Lines.fromPointAndAngle(Vector2D.ZERO, 0.0, TEST_PRECISION);
        final Line b = Lines.fromPointAndAngle(Vector2D.of(1, 4), Math.PI, TEST_PRECISION);
        final Line c = Lines.fromPointAndDirection(Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION);

        // act/assert
        Assertions.assertEquals(0.0, a.angle(a), TEST_EPS);
        Assertions.assertEquals(-Math.PI, a.angle(b), TEST_EPS);
        Assertions.assertEquals(0.25 * Math.PI, a.angle(c), TEST_EPS);

        Assertions.assertEquals(0.0, b.angle(b), TEST_EPS);
        Assertions.assertEquals(-Math.PI, b.angle(a), TEST_EPS);
        Assertions.assertEquals(-0.75 * Math.PI, b.angle(c), TEST_EPS);

        Assertions.assertEquals(0.0, c.angle(c), TEST_EPS);
        Assertions.assertEquals(-0.25 * Math.PI, c.angle(a), TEST_EPS);
        Assertions.assertEquals(0.75 * Math.PI, c.angle(b), TEST_EPS);
    }

    @Test
    void testProject() {
        // --- arrange
        final Line xAxis = Lines.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
        final Line yAxis = Lines.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_Y, TEST_PRECISION);

        final double diagonalYIntercept = 1;
        final Vector2D diagonalDir = Vector2D.of(1, 2);
        final Line diagonal = Lines.fromPointAndDirection(Vector2D.of(0, diagonalYIntercept), diagonalDir, TEST_PRECISION);

        EuclideanTestUtils.permute(-5, 5, 0.5, (x, y) -> {
            final 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);

            final Vector2D diagonalPt = diagonal.project(pt);
            Assertions.assertTrue(diagonal.contains(diagonalPt));
            Assertions.assertEquals(diagonal.distance(pt), pt.distance(diagonalPt), TEST_EPS);

            // check that y = mx + b is true
            Assertions.assertEquals(diagonalPt.getY(),
                    (diagonalDir.getY() * diagonalPt.getX() / diagonalDir.getX()) + diagonalYIntercept, TEST_EPS);
        });
    }

    @Test
    void testSpan() {
        // arrange
        final Line line = Lines.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);

        // act
        final LineConvexSubset result = line.span();

        // assert
        Assertions.assertSame(line, result.getHyperplane());
        Assertions.assertSame(line, result.getLine());
    }

    @Test
    void testSegment_doubles() {
        // arrange
        final Line line = Lines.fromPointAndAngle(Vector2D.of(0, 1), 0.0, TEST_PRECISION);

        // act
        final Segment segment = line.segment(1, 2);

        // assert
        Assertions.assertSame(line, segment.getLine());
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), segment.getStartPoint(), TEST_EPS);
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 1), segment.getEndPoint(), TEST_EPS);
    }

    @Test
    void testSegment_pointsOnLine() {
        // arrange
        final Line line = Lines.fromPointAndAngle(Vector2D.of(0, 1), 0.0, TEST_PRECISION);

        // act
        final Segment segment = line.segment(Vector2D.of(3, 1), Vector2D.of(2, 1));

        // assert
        Assertions.assertSame(line, segment.getLine());
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 1), segment.getStartPoint(), TEST_EPS);
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(3, 1), segment.getEndPoint(), TEST_EPS);
    }

    @Test
    void testSegment_pointsProjectedOnLine() {
        // arrange
        final Line line = Lines.fromPointAndAngle(Vector2D.of(0, 1), 0.0, TEST_PRECISION);

        // act
        final Segment segment = line.segment(Vector2D.of(-3, 2), Vector2D.of(2, -1));

        // assert
        Assertions.assertSame(line, segment.getLine());
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-3, 1), segment.getStartPoint(), TEST_EPS);
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 1), segment.getEndPoint(), TEST_EPS);
    }

    @Test
    void testLineTo_pointOnLine() {
        // arrange
        final Line line = Lines.fromPointAndAngle(Vector2D.of(0, 1), Math.PI, TEST_PRECISION);

        // act
        final ReverseRay halfLine = line.reverseRayTo(Vector2D.of(-3, 1));

        // assert
        Assertions.assertSame(line, halfLine.getLine());
        Assertions.assertTrue(halfLine.isInfinite());
        Assertions.assertNull(halfLine.getStartPoint());
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-3, 1), halfLine.getEndPoint(), TEST_EPS);

        Assertions.assertTrue(halfLine.contains(Vector2D.of(1, 1)));
        Assertions.assertFalse(halfLine.contains(Vector2D.of(-4, 1)));
    }

    @Test
    void testLineTo_pointProjectedOnLine() {
        // arrange
        final Line line = Lines.fromPointAndAngle(Vector2D.of(0, 1), Math.PI, TEST_PRECISION);

        // act
        final ReverseRay halfLine = line.reverseRayTo(Vector2D.of(-3, 5));

        // assert
        Assertions.assertSame(line, halfLine.getLine());
        Assertions.assertTrue(halfLine.isInfinite());
        Assertions.assertNull(halfLine.getStartPoint());
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-3, 1), halfLine.getEndPoint(), TEST_EPS);

        Assertions.assertTrue(halfLine.contains(Vector2D.of(1, 1)));
        Assertions.assertFalse(halfLine.contains(Vector2D.of(-4, 1)));
    }

    @Test
    void testLineTo_double() {
        // arrange
        final Line line = Lines.fromPointAndAngle(Vector2D.of(0, 1), Math.PI, TEST_PRECISION);

        // act
        final ReverseRay halfLine = line.reverseRayTo(-1);

        // assert
        Assertions.assertSame(line, halfLine.getLine());
        Assertions.assertTrue(halfLine.isInfinite());
        Assertions.assertNull(halfLine.getStartPoint());
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), halfLine.getEndPoint(), TEST_EPS);

        Assertions.assertTrue(halfLine.contains(Vector2D.of(2, 1)));
        Assertions.assertFalse(halfLine.contains(Vector2D.of(-4, 1)));
    }

    @Test
    void testRayFrom_pointOnLine() {
        // arrange
        final Line line = Lines.fromPointAndAngle(Vector2D.of(0, 1), Math.PI, TEST_PRECISION);

        // act
        final Ray ray = line.rayFrom(Vector2D.of(-3, 1));

        // assert
        Assertions.assertSame(line, ray.getLine());
        Assertions.assertTrue(ray.isInfinite());
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-3, 1), ray.getStartPoint(), TEST_EPS);
        Assertions.assertNull(ray.getEndPoint());

        Assertions.assertFalse(ray.contains(Vector2D.of(1, 1)));
        Assertions.assertTrue(ray.contains(Vector2D.of(-4, 1)));
    }

    @Test
    void testRayFrom_pointProjectedOnLine() {
        // arrange
        final Line line = Lines.fromPointAndAngle(Vector2D.of(0, 1), Math.PI, TEST_PRECISION);

        // act
        final Ray ray = line.rayFrom(Vector2D.of(-3, 5));

        // assert
        Assertions.assertSame(line, ray.getLine());
        Assertions.assertTrue(ray.isInfinite());
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-3, 1), ray.getStartPoint(), TEST_EPS);
        Assertions.assertNull(ray.getEndPoint());

        Assertions.assertFalse(ray.contains(Vector2D.of(1, 1)));
        Assertions.assertTrue(ray.contains(Vector2D.of(-4, 1)));
    }

    @Test
    void testRayFrom_double() {
        // arrange
        final Line line = Lines.fromPointAndAngle(Vector2D.of(0, 1), Math.PI, TEST_PRECISION);

        // act
        final Ray ray = line.rayFrom(-1);

        // assert
        Assertions.assertSame(line, ray.getLine());
        Assertions.assertTrue(ray.isInfinite());
        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), ray.getStartPoint(), TEST_EPS);
        Assertions.assertNull(ray.getEndPoint());

        Assertions.assertFalse(ray.contains(Vector2D.of(2, 1)));
        Assertions.assertTrue(ray.contains(Vector2D.of(-4, 1)));
    }

    @Test
    void testOffset_parallelLines() {
        // arrange
        final double dist = Math.sin(Math.atan2(2, 1));

        final Line a = Lines.fromPoints(Vector2D.of(-2, 0), Vector2D.of(0, 4), TEST_PRECISION);
        final Line b = Lines.fromPoints(Vector2D.of(-3, 0), Vector2D.of(0, 6), TEST_PRECISION);
        final Line c = Lines.fromPoints(Vector2D.of(-1, 0), Vector2D.of(0, 2), TEST_PRECISION);
        final Line d = Lines.fromPoints(Vector2D.of(1, 0), Vector2D.of(0, -2), TEST_PRECISION);

        // act/assert
        Assertions.assertEquals(-dist, a.offset(b), TEST_EPS);
        Assertions.assertEquals(dist, b.offset(a), TEST_EPS);

        Assertions.assertEquals(dist, a.offset(c), TEST_EPS);
        Assertions.assertEquals(-dist, c.offset(a), TEST_EPS);

        Assertions.assertEquals(3 * dist, a.offset(d), TEST_EPS);
        Assertions.assertEquals(3 * dist, d.offset(a), TEST_EPS);
    }

    @Test
    void testOffset_coincidentLines() {
        // arrange
        final Line a = Lines.fromPoints(Vector2D.of(-2, 0), Vector2D.of(0, 4), TEST_PRECISION);
        final Line b = Lines.fromPoints(Vector2D.of(-2, 0), Vector2D.of(0, 4), TEST_PRECISION);
        final Line c = b.reverse();

        // act/assert
        Assertions.assertEquals(0, a.offset(a), TEST_EPS);

        Assertions.assertEquals(0, a.offset(b), TEST_EPS);
        Assertions.assertEquals(0, b.offset(a), TEST_EPS);

        Assertions.assertEquals(0, a.offset(c), TEST_EPS);
        Assertions.assertEquals(0, c.offset(a), TEST_EPS);
    }

    @Test
    void testOffset_nonParallelLines() {
        // arrange
        final Line a = Lines.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
        final Line b = Lines.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_Y, TEST_PRECISION);
        final Line c = Lines.fromPoints(Vector2D.of(-1, 0), Vector2D.of(0, 2), TEST_PRECISION);
        final Line d = Lines.fromPoints(Vector2D.of(1, 0), Vector2D.of(0, 4), TEST_PRECISION);

        // act/assert
        Assertions.assertEquals(0, a.offset(b), TEST_EPS);
        Assertions.assertEquals(0, b.offset(a), TEST_EPS);

        Assertions.assertEquals(0, a.offset(c), TEST_EPS);
        Assertions.assertEquals(0, c.offset(a), TEST_EPS);

        Assertions.assertEquals(0, a.offset(d), TEST_EPS);
        Assertions.assertEquals(0, d.offset(a), TEST_EPS);
    }

    @Test
    void testOffset_point() {
        // arrange
        final Line line = Lines.fromPoints(Vector2D.of(-1, 0), Vector2D.of(0, 2), TEST_PRECISION);
        final Line reversed = line.reverse();

        // act/assert
        Assertions.assertEquals(0.0, line.offset(Vector2D.of(-0.5, 1)), TEST_EPS);
        Assertions.assertEquals(0.0, line.offset(Vector2D.of(-1.5, -1)), TEST_EPS);
        Assertions.assertEquals(0.0, line.offset(Vector2D.of(0.5, 3)), TEST_EPS);

        final double d = Math.sin(Math.atan2(2, 1));

        Assertions.assertEquals(d, line.offset(Vector2D.ZERO), TEST_EPS);
        Assertions.assertEquals(-d, line.offset(Vector2D.of(-1, 2)), TEST_EPS);

        Assertions.assertEquals(-d, reversed.offset(Vector2D.ZERO), TEST_EPS);
        Assertions.assertEquals(d, reversed.offset(Vector2D.of(-1, 2)), TEST_EPS);
    }

    @Test
    void testOffset_point_permute() {
        // arrange
        final Line line = Lines.fromPoints(Vector2D.of(-1, 0), Vector2D.of(0, 2), TEST_PRECISION);
        final Vector2D lineOrigin = line.getOrigin();

        EuclideanTestUtils.permute(-5, 5, 0.5, (x, y) -> {
            final Vector2D pt = Vector2D.of(x, y);

            // act
            final double offset = line.offset(pt);

            // arrange
            final Vector2D vec = lineOrigin.vectorTo(pt).reject(line.getDirection());
            final double dot = vec.dot(line.getOffsetDirection());
            final double expected = Math.signum(dot) * vec.norm();

            Assertions.assertEquals(expected, offset, TEST_EPS);
        });
    }

    @Test
    void testSimilarOrientation() {
        // arrange
        final Line a = Lines.fromPointAndAngle(Vector2D.ZERO, 0.0, TEST_PRECISION);
        final Line b = Lines.fromPointAndAngle(Vector2D.of(4, 5), 0.0, TEST_PRECISION);
        final Line c = Lines.fromPointAndAngle(Vector2D.of(-1, -3), 0.4 * Math.PI, TEST_PRECISION);
        final Line d = Lines.fromPointAndAngle(Vector2D.of(1, 0), -0.4 * Math.PI, TEST_PRECISION);

        final Line e = Lines.fromPointAndAngle(Vector2D.of(6, -3), Math.PI, TEST_PRECISION);
        final Line f = Lines.fromPointAndAngle(Vector2D.of(8, 5), 0.8 * Math.PI, TEST_PRECISION);
        final Line g = Lines.fromPointAndAngle(Vector2D.of(6, -3), -0.8 * Math.PI, TEST_PRECISION);

        // act/assert
        Assertions.assertTrue(a.similarOrientation(a));
        Assertions.assertTrue(a.similarOrientation(b));
        Assertions.assertTrue(b.similarOrientation(a));
        Assertions.assertTrue(a.similarOrientation(c));
        Assertions.assertTrue(c.similarOrientation(a));
        Assertions.assertTrue(a.similarOrientation(d));
        Assertions.assertTrue(d.similarOrientation(a));

        Assertions.assertFalse(c.similarOrientation(d));
        Assertions.assertFalse(d.similarOrientation(c));

        Assertions.assertTrue(e.similarOrientation(f));
        Assertions.assertTrue(f.similarOrientation(e));
        Assertions.assertTrue(e.similarOrientation(g));
        Assertions.assertTrue(g.similarOrientation(e));

        Assertions.assertFalse(a.similarOrientation(e));
        Assertions.assertFalse(e.similarOrientation(a));
    }

    @Test
    void testSimilarOrientation_orthogonal() {
        // arrange
        final Line a = Lines.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
        final Line b = Lines.fromPointAndDirection(Vector2D.of(4, 5), Vector2D.Unit.PLUS_Y, TEST_PRECISION);
        final Line c = Lines.fromPointAndDirection(Vector2D.of(-4, -5), Vector2D.Unit.MINUS_Y, TEST_PRECISION);

        // act/assert
        Assertions.assertTrue(a.similarOrientation(b));
        Assertions.assertTrue(b.similarOrientation(a));
        Assertions.assertTrue(a.similarOrientation(c));
        Assertions.assertTrue(c.similarOrientation(a));
    }

    @Test
    void testDistance_parallelLines() {
        // arrange
        final double dist = Math.sin(Math.atan2(2, 1));

        final Line a = Lines.fromPoints(Vector2D.of(-2, 0), Vector2D.of(0, 4), TEST_PRECISION);
        final Line b = Lines.fromPoints(Vector2D.of(-3, 0), Vector2D.of(0, 6), TEST_PRECISION);
        final Line c = Lines.fromPoints(Vector2D.of(-1, 0), Vector2D.of(0, 2), TEST_PRECISION);
        final Line d = Lines.fromPoints(Vector2D.of(1, 0), Vector2D.of(0, -2), TEST_PRECISION);

        // act/assert
        Assertions.assertEquals(dist, a.distance(b), TEST_EPS);
        Assertions.assertEquals(dist, b.distance(a), TEST_EPS);

        Assertions.assertEquals(dist, a.distance(c), TEST_EPS);
        Assertions.assertEquals(dist, c.distance(a), TEST_EPS);

        Assertions.assertEquals(3 * dist, a.distance(d), TEST_EPS);
        Assertions.assertEquals(3 * dist, d.distance(a), TEST_EPS);
    }

    @Test
    void testDistance_coincidentLines() {
        // arrange
        final Line a = Lines.fromPoints(Vector2D.of(-2, 0), Vector2D.of(0, 4), TEST_PRECISION);
        final Line b = Lines.fromPoints(Vector2D.of(-2, 0), Vector2D.of(0, 4), TEST_PRECISION);
        final Line c = b.reverse();

        // act/assert
        Assertions.assertEquals(0, a.distance(a), TEST_EPS);

        Assertions.assertEquals(0, a.distance(b), TEST_EPS);
        Assertions.assertEquals(0, b.distance(a), TEST_EPS);

        Assertions.assertEquals(0, a.distance(c), TEST_EPS);
        Assertions.assertEquals(0, c.distance(a), TEST_EPS);
    }

    @Test
    void testDistance_nonParallelLines() {
        // arrange
        final Line a = Lines.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
        final Line b = Lines.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_Y, TEST_PRECISION);
        final Line c = Lines.fromPoints(Vector2D.of(-1, 0), Vector2D.of(0, 2), TEST_PRECISION);
        final Line d = Lines.fromPoints(Vector2D.of(1, 0), Vector2D.of(0, 4), TEST_PRECISION);

        // act/assert
        Assertions.assertEquals(0, a.distance(b), TEST_EPS);
        Assertions.assertEquals(0, b.distance(a), TEST_EPS);

        Assertions.assertEquals(0, a.distance(c), TEST_EPS);
        Assertions.assertEquals(0, c.distance(a), TEST_EPS);

        Assertions.assertEquals(0, a.distance(d), TEST_EPS);
        Assertions.assertEquals(0, d.distance(a), TEST_EPS);
    }

    @Test
    void testDistance() {
        // arrange
        final Line line = Lines.fromPoints(Vector2D.of(2, 1), Vector2D.of(-2, -2), TEST_PRECISION);

        // act/assert
        Assertions.assertEquals(0, line.distance(line.getOrigin()), TEST_EPS);
        Assertions.assertEquals(+5.0, line.distance(Vector2D.of(5, -3)), TEST_EPS);
        Assertions.assertEquals(+5.0, line.distance(Vector2D.of(-5, 2)), TEST_EPS);
    }

    @Test
    void testPointAt() {
        // arrange
        final Vector2D origin = Vector2D.of(-1, 1);
        final double d = Math.sqrt(2);
        final Line line = Lines.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
    void testPointAt_abscissaOffsetRoundtrip() {
        // arrange
        final Line line = Lines.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
                final Vector2D point = line.pointAt(abscissa, offset);

                // assert
                Assertions.assertEquals(abscissa, line.toSubspace(point).getX(), TEST_EPS);
                Assertions.assertEquals(offset, line.offset(point), TEST_EPS);
            }
        }
    }

    @Test
    void testContains_line() {
        // arrange
        final Vector2D pt = Vector2D.of(1, 2);
        final Vector2D dir = Vector2D.of(3, 7);
        final Line a = Lines.fromPointAndDirection(pt, dir, TEST_PRECISION);
        final Line b = Lines.fromPointAndDirection(Vector2D.of(0, -4), dir, TEST_PRECISION);
        final Line c = Lines.fromPointAndDirection(Vector2D.of(-2, -2), dir.negate(), TEST_PRECISION);
        final Line d = Lines.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);

        final Line e = Lines.fromPointAndDirection(pt, dir, TEST_PRECISION);
        final Line f = Lines.fromPointAndDirection(pt, dir.negate(), TEST_PRECISION);

        // act/assert
        Assertions.assertTrue(a.contains(a));

        Assertions.assertTrue(a.contains(e));
        Assertions.assertTrue(e.contains(a));

        Assertions.assertTrue(a.contains(f));
        Assertions.assertTrue(f.contains(a));

        Assertions.assertFalse(a.contains(b));
        Assertions.assertFalse(a.contains(c));
        Assertions.assertFalse(a.contains(d));
    }

    @Test
    void testIsParallel_closeToEpsilon() {
        // arrange
        final double eps = 1e-3;
        final Precision.DoubleEquivalence precision = Precision.doubleEquivalenceOfEpsilon(eps);

        final Vector2D p = Vector2D.of(1, 2);

        final Line line = Lines.fromPointAndAngle(p, 0.0, precision);

        // act/assert
        final Vector2D offset1 = Vector2D.of(0, 1e-4);
        final Vector2D offset2 = Vector2D.of(0, 2e-3);

        Assertions.assertTrue(line.contains(Lines.fromPointAndAngle(p.add(offset1), 0.0, precision)));
        Assertions.assertTrue(line.contains(Lines.fromPointAndAngle(p.subtract(offset1), 0.0, precision)));

        Assertions.assertFalse(line.contains(Lines.fromPointAndAngle(p.add(offset2), 0.0, precision)));
        Assertions.assertFalse(line.contains(Lines.fromPointAndAngle(p.subtract(offset2), 0.0, precision)));

        Assertions.assertTrue(line.contains(Lines.fromPointAndAngle(p, 1e-4, precision)));
        Assertions.assertFalse(line.contains(Lines.fromPointAndAngle(p, 1e-2, precision)));
    }

    @Test
    void testContains_point() {
        // arrange
        final Vector2D p1 = Vector2D.of(-1, 0);
        final Vector2D p2 = Vector2D.of(0, 2);
        final Line line = Lines.fromPoints(p1, p2, TEST_PRECISION);

        // act/assert
        Assertions.assertTrue(line.contains(p1));
        Assertions.assertTrue(line.contains(p2));

        Assertions.assertFalse(line.contains(Vector2D.ZERO));
        Assertions.assertFalse(line.contains(Vector2D.of(100, 79)));

        final Vector2D offset1 = Vector2D.of(0.1, 0);
        final Vector2D offset2 = Vector2D.of(0, -0.1);
        Vector2D v;
        for (double t = -2; t <= 2; t += 0.1) {
            v = p1.lerp(p2, t);

            Assertions.assertTrue(line.contains(v));

            Assertions.assertFalse(line.contains(v.add(offset1)));
            Assertions.assertFalse(line.contains(v.add(offset2)));
        }
    }

    @Test
    void testContains_point_closeToEpsilon() {
        // arrange
        final double eps = 1e-3;
        final Precision.DoubleEquivalence precision = Precision.doubleEquivalenceOfEpsilon(eps);

        final Vector2D p1 = Vector2D.of(-1, 0);
        final Vector2D p2 = Vector2D.of(0, 2);
        final Vector2D mid = p1.lerp(p2, 0.5);

        final Line line = Lines.fromPoints(p1, p2, precision);
        final Vector2D dir = line.getOffsetDirection();

        // act/assert
        Assertions.assertTrue(line.contains(mid.add(dir.multiply(1e-4))));
        Assertions.assertTrue(line.contains(mid.add(dir.multiply(-1e-4))));

        Assertions.assertFalse(line.contains(mid.add(dir.multiply(2e-3))));
        Assertions.assertFalse(line.contains(mid.add(dir.multiply(-2e-3))));
    }

    @Test
    void testDistance_point() {
        // arrange
        final Line line = Lines.fromPoints(Vector2D.of(-1, 0), Vector2D.of(0, 2), TEST_PRECISION);
        final Line reversed = line.reverse();

        // act/assert
        Assertions.assertEquals(0.0, line.distance(Vector2D.of(-0.5, 1)), TEST_EPS);
        Assertions.assertEquals(0.0, line.distance(Vector2D.of(-1.5, -1)), TEST_EPS);
        Assertions.assertEquals(0.0, line.distance(Vector2D.of(0.5, 3)), TEST_EPS);

        final double d = Math.sin(Math.atan2(2, 1));

        Assertions.assertEquals(d, line.distance(Vector2D.ZERO), TEST_EPS);
        Assertions.assertEquals(d, line.distance(Vector2D.of(-1, 2)), TEST_EPS);

        Assertions.assertEquals(d, reversed.distance(Vector2D.ZERO), TEST_EPS);
        Assertions.assertEquals(d, reversed.distance(Vector2D.of(-1, 2)), TEST_EPS);
    }

    @Test
    void testDistance_point_permute() {
        // arrange
        final Line line = Lines.fromPoints(Vector2D.of(-1, 0), Vector2D.of(0, 2), TEST_PRECISION);
        final Vector2D lineOrigin = line.getOrigin();

        EuclideanTestUtils.permute(-5, 5, 0.5, (x, y) -> {
            final Vector2D pt = Vector2D.of(x, y);

            // act
            final double dist = line.distance(pt);

            // arrange
            final Vector2D vec = lineOrigin.vectorTo(pt).reject(line.getDirection());
            Assertions.assertEquals(vec.norm(), dist, TEST_EPS);
        });
    }

    @Test
    void testIsParallel() {
        // arrange
        final Vector2D dir = Vector2D.of(3, 7);
        final Line a = Lines.fromPointAndDirection(Vector2D.of(1, 2), dir, TEST_PRECISION);
        final Line b = Lines.fromPointAndDirection(Vector2D.of(0, -4), dir, TEST_PRECISION);
        final Line c = Lines.fromPointAndDirection(Vector2D.of(-2, -2), dir.negate(), TEST_PRECISION);
        final Line d = Lines.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);

        // act/assert
        Assertions.assertTrue(a.isParallel(a));

        Assertions.assertTrue(a.isParallel(b));
        Assertions.assertTrue(b.isParallel(a));

        Assertions.assertTrue(a.isParallel(c));
        Assertions.assertTrue(c.isParallel(a));

        Assertions.assertFalse(a.isParallel(d));
        Assertions.assertFalse(d.isParallel(a));
    }

    @Test
    void testIsParallel_closeToParallel() {
        // arrange
        final double eps = 1e-3;
        final Precision.DoubleEquivalence precision = Precision.doubleEquivalenceOfEpsilon(eps);

        final Vector2D p1 = Vector2D.of(1, 2);
        final Vector2D p2 = Vector2D.of(1, -2);

        final Line line = Lines.fromPointAndAngle(p1, 0.0, precision);

        // act/assert
        Assertions.assertTrue(line.isParallel(Lines.fromPointAndAngle(p2, 1e-4, precision)));
        Assertions.assertFalse(line.isParallel(Lines.fromPointAndAngle(p2, 1e-2, precision)));
    }

    @Test
    void testTransform() {
        // arrange
        final AffineTransformMatrix2D scale = AffineTransformMatrix2D.createScale(2, 3);
        final AffineTransformMatrix2D reflect = AffineTransformMatrix2D.createScale(-1, 1);
        final AffineTransformMatrix2D translate = AffineTransformMatrix2D.createTranslation(3, 4);
        final AffineTransformMatrix2D rotate = AffineTransformMatrix2D.createRotation(Angle.PI_OVER_TWO);
        final AffineTransformMatrix2D rotateAroundPt = AffineTransformMatrix2D.createRotation(Vector2D.of(0, 1), Angle.PI_OVER_TWO);

        final Vector2D p1 = Vector2D.of(0, 1);
        final Vector2D p2 = Vector2D.of(1, 0);

        final Line horizontal = Lines.fromPointAndDirection(p1, Vector2D.Unit.PLUS_X, TEST_PRECISION);
        final Line vertical = Lines.fromPointAndDirection(p2, Vector2D.Unit.PLUS_Y, TEST_PRECISION);
        final Line diagonal = Lines.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION);

        // act/assert
        Assertions.assertSame(TEST_PRECISION, horizontal.transform(scale).getPrecision());

        checkLine(horizontal.transform(scale), Vector2D.of(0, 3), Vector2D.Unit.PLUS_X);
        checkLine(vertical.transform(scale), Vector2D.of(2, 0), Vector2D.Unit.PLUS_Y);
        checkLine(diagonal.transform(scale), Vector2D.ZERO, Vector2D.of(2, 3).normalize());

        checkLine(horizontal.transform(reflect), p1, Vector2D.Unit.MINUS_X);
        checkLine(vertical.transform(reflect), Vector2D.of(-1, 0), Vector2D.Unit.PLUS_Y);
        checkLine(diagonal.transform(reflect), Vector2D.ZERO, Vector2D.of(-1, 1).normalize());

        checkLine(horizontal.transform(translate), Vector2D.of(0, 5), Vector2D.Unit.PLUS_X);
        checkLine(vertical.transform(translate), Vector2D.of(4, 0), Vector2D.Unit.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.Unit.PLUS_Y);
        checkLine(vertical.transform(rotate), Vector2D.of(0, 1), Vector2D.Unit.MINUS_X);
        checkLine(diagonal.transform(rotate), Vector2D.ZERO, Vector2D.of(-1, 1).normalize());

        checkLine(horizontal.transform(rotateAroundPt), Vector2D.ZERO, Vector2D.Unit.PLUS_Y);
        checkLine(vertical.transform(rotateAroundPt), Vector2D.of(0, 2), Vector2D.Unit.MINUS_X);
        checkLine(diagonal.transform(rotateAroundPt), Vector2D.of(1, 1), Vector2D.of(-1, 1).normalize());
    }

    @Test
    void testTransform_collapsedPoints() {
        // arrange
        final AffineTransformMatrix2D scaleCollapse = AffineTransformMatrix2D.createScale(0, 1);
        final Line line = Lines.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);

        // act/assert
        GeometryTestUtils.assertThrowsWithMessage(() -> {
            line.transform(scaleCollapse);
        }, IllegalArgumentException.class, "Line direction cannot be zero");
    }

    @Test
    void testSubspaceTransform() {
        // arrange
        final Line line = Lines.fromPoints(Vector2D.of(1, 0), Vector2D.of(1, 1), TEST_PRECISION);

        // act/assert
        checkSubspaceTransform(line.subspaceTransform(AffineTransformMatrix2D.createScale(2, 3)),
                Vector2D.of(2, 0), Vector2D.Unit.PLUS_Y,
                Vector2D.of(2, 0), Vector2D.of(2, 3));

        checkSubspaceTransform(line.subspaceTransform(AffineTransformMatrix2D.createTranslation(2, 3)),
                Vector2D.of(3, 0), Vector2D.Unit.PLUS_Y,
                Vector2D.of(3, 3), Vector2D.of(3, 4));

        checkSubspaceTransform(line.subspaceTransform(AffineTransformMatrix2D.createRotation(Angle.PI_OVER_TWO)),
                Vector2D.of(0, 1), Vector2D.Unit.MINUS_X,
                Vector2D.of(0, 1), Vector2D.of(-1, 1));
    }

    private void checkSubspaceTransform(final SubspaceTransform st, final Vector2D origin, final Vector2D dir, final Vector2D tZero, final Vector2D tOne) {

        final Line line = st.getLine();
        final AffineTransformMatrix1D transform = st.getTransform();

        checkLine(line, origin, dir);

        EuclideanTestUtils.assertCoordinatesEqual(tZero, line.toSpace(transform.apply(Vector1D.ZERO)), TEST_EPS);
        EuclideanTestUtils.assertCoordinatesEqual(tOne, line.toSpace(transform.apply(Vector1D.Unit.PLUS)), TEST_EPS);
    }

    @Test
    void testSubspaceTransform_transformsPointsCorrectly() {
        // arrange
        final Line line = Lines.fromPointAndDirection(Vector2D.of(1, 0), Vector2D.of(1, 1), TEST_PRECISION);

        EuclideanTestUtils.permuteSkipZero(-2, 2, 0.5, (a, b) -> {
            // create a somewhat complicate transform to try to hit all of the edge cases
            final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createTranslation(Vector2D.of(a, b))
                    .rotate(a * b)
                    .scale(0.1, 4);

            // act
            final SubspaceTransform st = line.subspaceTransform(transform);

            // assert
            for (double x = -5.0; x <= 5.0; x += 1) {
                final Vector1D subPt = Vector1D.of(x);
                final Vector2D expected = transform.apply(line.toSpace(subPt));
                final Vector2D actual = st.getLine().toSpace(
                        st.getTransform().apply(subPt));

                EuclideanTestUtils.assertCoordinatesEqual(expected, actual, TEST_EPS);
            }
        });
    }

    @Test
    void testEq() {
        // arrange
        final Precision.DoubleEquivalence precision = Precision.doubleEquivalenceOfEpsilon(1e-3);

        final Vector2D p = Vector2D.of(1, 2);
        final double angle = 1.0;

        final Line a = Lines.fromPointAndAngle(p, angle, precision);
        final Line b = Lines.fromPointAndAngle(Vector2D.ZERO, angle, precision);
        final Line c = Lines.fromPointAndAngle(p, angle + 1.0, precision);

        final Line d = Lines.fromPointAndAngle(p, angle, precision);
        final Line e = Lines.fromPointAndAngle(p.add(Vector2D.of(1e-4, 1e-4)), angle, precision);
        final Line f = Lines.fromPointAndAngle(p, angle + 1e-4, precision);

        // act/assert
        Assertions.assertTrue(a.eq(a, precision));

        Assertions.assertTrue(a.eq(d, precision));
        Assertions.assertTrue(d.eq(a, precision));

        Assertions.assertTrue(a.eq(e, precision));
        Assertions.assertTrue(e.eq(a, precision));

        Assertions.assertTrue(a.eq(f, precision));
        Assertions.assertTrue(f.eq(a, precision));

        Assertions.assertFalse(a.eq(b, precision));
        Assertions.assertFalse(a.eq(c, precision));
    }

    @Test
    void testHashCode() {
        // arrange
        final Precision.DoubleEquivalence precision1 = Precision.doubleEquivalenceOfEpsilon(1e-4);
        final Precision.DoubleEquivalence precision2 = Precision.doubleEquivalenceOfEpsilon(1e-5);

        final Vector2D p = Vector2D.of(1, 2);
        final Vector2D v = Vector2D.of(1, 1);

        final Line a = Lines.fromPointAndDirection(p, v, precision1);
        final Line b = Lines.fromPointAndDirection(Vector2D.ZERO, v, precision1);
        final Line c = Lines.fromPointAndDirection(p, v.negate(), precision1);
        final Line d = Lines.fromPointAndDirection(p, v, precision2);
        final Line e = Lines.fromPointAndDirection(p, v, precision1);

        // act/assert
        final int aHash = a.hashCode();

        Assertions.assertEquals(aHash, a.hashCode());
        Assertions.assertEquals(aHash, e.hashCode());

        Assertions.assertNotEquals(aHash, b.hashCode());
        Assertions.assertNotEquals(aHash, c.hashCode());
        Assertions.assertNotEquals(aHash, d.hashCode());
    }

    @Test
    void testEquals() {
     // arrange
        final Precision.DoubleEquivalence precision1 = Precision.doubleEquivalenceOfEpsilon(1e-4);
        final Precision.DoubleEquivalence precision2 = Precision.doubleEquivalenceOfEpsilon(1e-5);

        final Vector2D p = Vector2D.of(1, 2);
        final Vector2D v = Vector2D.of(1, 1);

        final Line a = Lines.fromPointAndDirection(p, v, precision1);
        final Line b = Lines.fromPointAndDirection(Vector2D.ZERO, v, precision1);
        final Line c = Lines.fromPointAndDirection(p, v.negate(), precision1);
        final Line d = Lines.fromPointAndDirection(p, v, precision2);
        final Line e = Lines.fromPointAndDirection(p, v, precision1);

        // act/assert
        GeometryTestUtils.assertSimpleEqualsCases(a);
        Assertions.assertEquals(a, e);
        Assertions.assertEquals(e, a);

        Assertions.assertNotEquals(a, b);
        Assertions.assertNotEquals(a, c);
        Assertions.assertNotEquals(a, d);
    }

    @Test
    void testToString() {
        // arrange
        final Line line = Lines.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);

        // act
        final String str = line.toString();

        // assert
        Assertions.assertTrue(str.contains("Line"));
        Assertions.assertTrue(str.contains("origin= (0.0, 0.0)"));
        Assertions.assertTrue(str.contains("direction= (1.0, 0.0)"));
    }

    /**
     * Check that the line has the given defining properties.
     * @param line
     * @param origin
     * @param dir
     */
    private void checkLine(final Line line, final Vector2D origin, final Vector2D dir) {
        EuclideanTestUtils.assertCoordinatesEqual(origin, line.getOrigin(), TEST_EPS);
        EuclideanTestUtils.assertCoordinatesEqual(dir, line.getDirection(), TEST_EPS);
    }
}