AbstractConvexHyperplaneBoundedRegionTest.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.core.partitioning;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.apache.commons.geometry.core.GeometryTestUtils;
import org.apache.commons.geometry.core.Region;
import org.apache.commons.geometry.core.RegionLocation;
import org.apache.commons.geometry.core.Transform;
import org.apache.commons.geometry.core.partitioning.test.PartitionTestUtils;
import org.apache.commons.geometry.core.partitioning.test.TestLine;
import org.apache.commons.geometry.core.partitioning.test.TestLineSegment;
import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
import org.apache.commons.geometry.core.partitioning.test.TestTransform2D;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

class AbstractConvexHyperplaneBoundedRegionTest {

    @Test
    void testBoundaries_areUnmodifiable() {
        // arrange
        final StubRegion region = new StubRegion(new ArrayList<>());
        final List<TestLineSegment> boundaries = region.getBoundaries();
        final TestLineSegment span = TestLine.X_AXIS.span();


        // act/assert
        Assertions.assertThrows(UnsupportedOperationException.class, () ->  boundaries.add(span));
    }

    @Test
    void testFull() {
        // act
        final StubRegion region = new StubRegion(Collections.emptyList());

        // assert
        Assertions.assertTrue(region.isFull());
        Assertions.assertFalse(region.isEmpty());
    }

    @Test
    void testGetBoundarySize() {
        // arrange
        final TestPoint2D p1 = new TestPoint2D(1, 0);
        final TestPoint2D p2 = new TestPoint2D(2, 0);
        final TestPoint2D p3 = new TestPoint2D(1, 1);

        // act/assert
        Assertions.assertEquals(0, new StubRegion(Collections.emptyList()).getBoundarySize(), PartitionTestUtils.EPS);
        GeometryTestUtils.assertPositiveInfinity(new StubRegion(Collections.singletonList(TestLine.X_AXIS.span())).getBoundarySize());
        Assertions.assertEquals(2 + Math.sqrt(2), new StubRegion(Arrays.asList(
                    new TestLineSegment(p1, p2),
                    new TestLineSegment(p2, p3),
                    new TestLineSegment(p3, p1)
                )).getBoundarySize(), PartitionTestUtils.EPS);
    }

    @Test
    void testClassify() {
        // arrange
        final TestPoint2D p1 = new TestPoint2D(1, 0);
        final TestPoint2D p2 = new TestPoint2D(2, 0);
        final TestPoint2D p3 = new TestPoint2D(1, 1);

        final StubRegion full = new StubRegion(Collections.emptyList());
        final StubRegion halfSpace = new StubRegion(Collections.singletonList(TestLine.X_AXIS.span()));
        final StubRegion triangle = new StubRegion(Arrays.asList(
                new TestLineSegment(p1, p2),
                new TestLineSegment(p2, p3),
                new TestLineSegment(p3, p1)
            ));

        // act/assert
        checkClassify(full, RegionLocation.INSIDE, TestPoint2D.ZERO, p1, p2, p3);

        checkClassify(halfSpace, RegionLocation.INSIDE, new TestPoint2D(0, 1));
        checkClassify(halfSpace, RegionLocation.OUTSIDE, new TestPoint2D(0, -1));
        checkClassify(halfSpace, RegionLocation.BOUNDARY,
                new TestPoint2D(-1, 0), new TestPoint2D(0, 0), new TestPoint2D(1, 0));

        checkClassify(triangle, RegionLocation.INSIDE, new TestPoint2D(1.25, 0.25));
        checkClassify(triangle, RegionLocation.OUTSIDE, new TestPoint2D(-1, 0), new TestPoint2D(0, 0), new TestPoint2D(3, 0));
        checkClassify(triangle, RegionLocation.BOUNDARY, p1, p2, p3);
    }

    @Test
    void testProject() {
        // arrange
        final TestPoint2D p1 = new TestPoint2D(1, 0);
        final TestPoint2D p2 = new TestPoint2D(2, 0);
        final TestPoint2D p3 = new TestPoint2D(1, 1);

        final StubRegion full = new StubRegion(Collections.emptyList());
        final StubRegion halfSpace = new StubRegion(Collections.singletonList(TestLine.X_AXIS.span()));
        final StubRegion triangle = new StubRegion(Arrays.asList(
                new TestLineSegment(p1, p2),
                new TestLineSegment(p2, p3),
                new TestLineSegment(p3, p1)
            ));

        // act/assert
        Assertions.assertNull(full.project(TestPoint2D.ZERO));
        Assertions.assertNull(full.project(new TestPoint2D(1, 1)));

        PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, halfSpace.project(new TestPoint2D(0, 1)));
        PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, halfSpace.project(new TestPoint2D(0, 0)));
        PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, halfSpace.project(new TestPoint2D(0, -1)));

        PartitionTestUtils.assertPointsEqual(new TestPoint2D(1.25, 0), triangle.project(new TestPoint2D(1.25, 0.1)));
        PartitionTestUtils.assertPointsEqual(p1, triangle.project(TestPoint2D.ZERO));
        PartitionTestUtils.assertPointsEqual(p3, triangle.project(new TestPoint2D(0, 10)));
    }

    @Test
    void testTrim() {
        // arrange
        final TestPoint2D p1 = new TestPoint2D(1, 0);
        final TestPoint2D p2 = new TestPoint2D(2, 0);
        final TestPoint2D p3 = new TestPoint2D(2, 1);
        final TestPoint2D p4 = new TestPoint2D(1, 1);

        final StubRegion full = new StubRegion(Collections.emptyList());
        final StubRegion halfSpace = new StubRegion(Collections.singletonList(TestLine.Y_AXIS.span()));
        final StubRegion square = new StubRegion(Arrays.asList(
                new TestLineSegment(p1, p2),
                new TestLineSegment(p2, p3),
                new TestLineSegment(p3, p4),
                new TestLineSegment(p4, p1)
            ));

        final TestLineSegment segment = new TestLineSegment(new TestPoint2D(-1, 0.5), new TestPoint2D(4, 0.5));

        // act/assert
        Assertions.assertSame(segment, full.trim(segment));

        final TestLineSegment trimmedA = halfSpace.trim(segment);
        PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 0.5), trimmedA.getStartPoint());
        PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0.5), trimmedA.getEndPoint());

        final TestLineSegment trimmedB = square.trim(segment);
        PartitionTestUtils.assertPointsEqual(new TestPoint2D(1, 0.5), trimmedB.getStartPoint());
        PartitionTestUtils.assertPointsEqual(new TestPoint2D(2, 0.5), trimmedB.getEndPoint());
    }

    @Test
    void testSplit_full() {
        // arrange
        final StubRegion region = new StubRegion(Collections.emptyList());

        final TestLine splitter = TestLine.X_AXIS;

        // act
        final Split<StubRegion> split = region.split(splitter);

        // assert
        Assertions.assertEquals(SplitLocation.BOTH, split.getLocation());

        final StubRegion minus = split.getMinus();
        Assertions.assertEquals(1, minus.getBoundaries().size());
        checkClassify(minus, RegionLocation.INSIDE, new TestPoint2D(0, 1));
        checkClassify(minus, RegionLocation.BOUNDARY, new TestPoint2D(0, 0));
        checkClassify(minus, RegionLocation.OUTSIDE, new TestPoint2D(0, -1));

        final StubRegion plus = split.getPlus();
        Assertions.assertEquals(1, plus.getBoundaries().size());
        checkClassify(plus, RegionLocation.OUTSIDE, new TestPoint2D(0, 1));
        checkClassify(plus, RegionLocation.BOUNDARY, new TestPoint2D(0, 0));
        checkClassify(plus, RegionLocation.INSIDE, new TestPoint2D(0, -1));
    }

    @Test
    void testSplit_parallel_splitterIsOutside_plusOnly() {
     // arrange
        final StubRegion region = new StubRegion(
                Collections.singletonList(new TestLineSegment(new TestPoint2D(0, 1), new TestPoint2D(1, 1))));

        final TestLine splitter = TestLine.X_AXIS.reverse();

        // act
        final Split<StubRegion> split = region.split(splitter);

        // assert
        Assertions.assertEquals(SplitLocation.PLUS, split.getLocation());

        Assertions.assertNull(split.getMinus());
        Assertions.assertSame(region, split.getPlus());
    }

    @Test
    void testSplit_parallel_splitterIsOutside_minusOnly() {
     // arrange
        final StubRegion region = new StubRegion(
                Collections.singletonList(new TestLineSegment(new TestPoint2D(0, 1), new TestPoint2D(1, 1))));

        final TestLine splitter = TestLine.X_AXIS;

        // act
        final Split<StubRegion> split = region.split(splitter);

        // assert
        Assertions.assertEquals(SplitLocation.MINUS, split.getLocation());

        Assertions.assertSame(region, split.getMinus());
        Assertions.assertNull(split.getPlus());
    }

    @Test
    void testSplit_parallel_splitterIsInside() {
     // arrange
        final StubRegion region = new StubRegion(
                Collections.singletonList(new TestLineSegment(new TestPoint2D(1, 1), new TestPoint2D(0, 1))));

        final TestLine splitter = TestLine.X_AXIS;

        // act
        final Split<StubRegion> split = region.split(splitter);

        // assert
        Assertions.assertEquals(SplitLocation.BOTH, split.getLocation());

        final TestPoint2D p1 = new TestPoint2D(0, 1.5);
        final TestPoint2D p2 = new TestPoint2D(0, 0.5);
        final TestPoint2D p3 = new TestPoint2D(0, -0.5);

        final StubRegion minus = split.getMinus();
        Assertions.assertEquals(2, minus.getBoundaries().size());
        checkClassify(minus, RegionLocation.INSIDE, p2);
        checkClassify(minus, RegionLocation.OUTSIDE, p1, p3);

        final StubRegion plus = split.getPlus();
        Assertions.assertEquals(1, plus.getBoundaries().size());
        checkClassify(plus, RegionLocation.INSIDE, p3);
        checkClassify(plus, RegionLocation.OUTSIDE, p1, p2);
    }

    @Test
    void testSplit_coincident_sameOrientation() {
     // arrange
        final StubRegion region = new StubRegion(Collections.singletonList(TestLine.X_AXIS.span()));

        final TestLine splitter = TestLine.X_AXIS;

        // act
        final Split<StubRegion> split = region.split(splitter);

        // assert
        Assertions.assertEquals(SplitLocation.MINUS, split.getLocation());

        Assertions.assertSame(region, split.getMinus());
        Assertions.assertNull(split.getPlus());
    }

    @Test
    void testSplit_coincident_oppositeOrientation() {
     // arrange
        final StubRegion region = new StubRegion(Collections.singletonList(TestLine.X_AXIS.span()));

        final TestLine splitter = TestLine.X_AXIS.reverse();

        // act
        final Split<StubRegion> split = region.split(splitter);

        // assert
        Assertions.assertEquals(SplitLocation.PLUS, split.getLocation());

        Assertions.assertNull(split.getMinus());
        Assertions.assertSame(region, split.getPlus());
    }

    @Test
    void testSplit_finite_both() {
        // arrange
        final TestPoint2D p1 = new TestPoint2D(1, -0.5);
        final TestPoint2D p2 = new TestPoint2D(2, -0.5);
        final TestPoint2D p3 = new TestPoint2D(2, 0.5);
        final TestPoint2D p4 = new TestPoint2D(1, 0.5);

        final StubRegion region = new StubRegion(Arrays.asList(
                    new TestLineSegment(p1, p2),
                    new TestLineSegment(p2, p3),
                    new TestLineSegment(p3, p4),
                    new TestLineSegment(p4, p1)
                ));

        final TestLine splitter = TestLine.X_AXIS;

        // act
        final Split<StubRegion> split = region.split(splitter);

        // assert
        Assertions.assertEquals(SplitLocation.BOTH, split.getLocation());

        final StubRegion minus = split.getMinus();
        Assertions.assertEquals(4, minus.getBoundaries().size());
        checkClassify(minus, RegionLocation.INSIDE, new TestPoint2D(1.5, 0.25));
        checkClassify(minus, RegionLocation.BOUNDARY, new TestPoint2D(1.5, 0));
        checkClassify(minus, RegionLocation.OUTSIDE, new TestPoint2D(1.5, -0.25));

        final StubRegion plus = split.getPlus();
        Assertions.assertEquals(4, plus.getBoundaries().size());
        checkClassify(plus, RegionLocation.OUTSIDE, new TestPoint2D(1.5, 0.25));
        checkClassify(plus, RegionLocation.BOUNDARY, new TestPoint2D(1.5, 0));
        checkClassify(plus, RegionLocation.INSIDE, new TestPoint2D(1.5, -0.25));
    }

    // The following tests are designed to check the situation where there are
    // inconsistencies between how a splitter splits a set of boundaries and how
    // the boundaries split the splitter. For example, no portion of the splitter
    // may lie inside the region (on the minus sides of all boundaries), but some
    // of the boundaries may be determined to lie on both sides of the splitter.
    // One potential cause of this situation is accumulated floating point errors.

    @Test
    void testSplit_inconsistentBoundarySplitLocations_minus() {
        // arrange
        final TestLine a = new TestLine(new TestPoint2D(0, 0), new TestPoint2D(1, 1));
        final TestLine b = new TestLine(new TestPoint2D(-1, 1), new TestPoint2D(0, 0));

        final StubRegion region = new StubRegion(Arrays.asList(
                    new TestLineSegment(-1e-8, Double.POSITIVE_INFINITY, a),
                    new TestLineSegment(Double.NEGATIVE_INFINITY, 1e-8, b)
                ));

        final List<TestLineSegment> segments = region.getBoundaries();
        PartitionTestUtils.assertPointsEqual(segments.get(0).getStartPoint(), segments.get(1).getEndPoint());

        final TestLine splitter = new TestLine(new TestPoint2D(0, 0), new TestPoint2D(1, 0));

        // act
        final Split<StubRegion> split = region.split(splitter);

        // assert
        Assertions.assertEquals(SplitLocation.MINUS, split.getLocation());
        Assertions.assertSame(region, split.getMinus());
        Assertions.assertNull(split.getPlus());
    }

    @Test
    void testSplit_inconsistentBoundarySplitLocations_plus() {
        // arrange
        final TestLine a = new TestLine(new TestPoint2D(0, 0), new TestPoint2D(1, 1));
        final TestLine b = new TestLine(new TestPoint2D(-1, 1), new TestPoint2D(0, 0));

        final StubRegion region = new StubRegion(Arrays.asList(
                    new TestLineSegment(-1e-8, Double.POSITIVE_INFINITY, a),
                    new TestLineSegment(Double.NEGATIVE_INFINITY, 1e-8, b)
                ));

        final List<TestLineSegment> segments = region.getBoundaries();
        PartitionTestUtils.assertPointsEqual(segments.get(0).getStartPoint(), segments.get(1).getEndPoint());

        final TestLine splitter = new TestLine(new TestPoint2D(1, 0), new TestPoint2D(0, 0));

        // act
        final Split<StubRegion> split = region.split(splitter);

        // assert
        Assertions.assertEquals(SplitLocation.PLUS, split.getLocation());
        Assertions.assertNull(split.getMinus());
        Assertions.assertSame(region, split.getPlus());
    }

    @Test
    void testSplit_inconsistentBoundarySplitLocations_trimmedNotNull_minus() {
        // arrange
        final TestLine a = new TestLine(new TestPoint2D(1e-8, 0), new TestPoint2D(1, 1));
        final TestLine b = new TestLine(new TestPoint2D(-1, 1), new TestPoint2D(-1e-8, 0));

        final StubRegion region = new StubRegion(Arrays.asList(
                    new TestLineSegment(1e-8, Double.POSITIVE_INFINITY, a),
                    new TestLineSegment(Double.NEGATIVE_INFINITY, -1e-8, b)
                ));

        final List<TestLineSegment> segments = region.getBoundaries();
        PartitionTestUtils.assertPointsEqual(segments.get(0).getStartPoint(), segments.get(1).getEndPoint());

        final TestLine splitter = new TestLine(new TestPoint2D(0, 0), new TestPoint2D(1, 0));

        // act
        final Split<StubRegion> split = region.split(splitter);

        // assert
        Assertions.assertEquals(SplitLocation.MINUS, split.getLocation());
        Assertions.assertSame(region, split.getMinus());
        Assertions.assertNull(split.getPlus());
    }

    @Test
    void testSplit_inconsistentBoundarySplitLocations_trimmedNotNull_plus() {
        // arrange
        final TestLine a = new TestLine(new TestPoint2D(1e-8, 0), new TestPoint2D(1, 1));
        final TestLine b = new TestLine(new TestPoint2D(-1, 1), new TestPoint2D(-1e-8, 0));

        final StubRegion region = new StubRegion(Arrays.asList(
                    new TestLineSegment(1e-8, Double.POSITIVE_INFINITY, a),
                    new TestLineSegment(Double.NEGATIVE_INFINITY, -1e-8, b)
                ));

        final List<TestLineSegment> segments = region.getBoundaries();
        PartitionTestUtils.assertPointsEqual(segments.get(0).getStartPoint(), segments.get(1).getEndPoint());

        final TestLine splitter = new TestLine(new TestPoint2D(0, 0), new TestPoint2D(-1, 0));

        // act
        final Split<StubRegion> split = region.split(splitter);

        // assert
        Assertions.assertEquals(SplitLocation.PLUS, split.getLocation());
        Assertions.assertNull(split.getMinus());
        Assertions.assertSame(region, split.getPlus());
    }

    @Test
    void testSplit_inconsistentBoundarySplitLocations_trimmedNotNull_neither() {
        // arrange
        final TestLine a = new TestLine(new TestPoint2D(1e-8, 0), new TestPoint2D(1, 1));
        final TestLine b = new TestLine(new TestPoint2D(-1, 1), new TestPoint2D(-1e-8, 0));

        final StubRegion region = new StubRegion(Arrays.asList(
                    new TestLineSegment(0, 0, a),
                    new TestLineSegment(0, 0, b)
                ));

        final List<TestLineSegment> segments = region.getBoundaries();
        PartitionTestUtils.assertPointsEqual(segments.get(0).getStartPoint(), segments.get(1).getEndPoint());

        final TestLine splitter = new TestLine(new TestPoint2D(0, 0), new TestPoint2D(1, 0));

        // act
        final Split<StubRegion> split = region.split(splitter);

        // assert
        Assertions.assertEquals(SplitLocation.NEITHER, split.getLocation());
        Assertions.assertNull(split.getMinus());
        Assertions.assertNull(split.getPlus());
    }

    @Test
    void testTransform_full() {
        // arrange
        final StubRegion region = new StubRegion(Collections.emptyList());

        final Transform<TestPoint2D> transform = new TestTransform2D(p -> new TestPoint2D(p.getX() + 1, p.getY() + 2));

        // act
        final StubRegion transformed = region.transform(transform);

        // assert
        Assertions.assertTrue(transformed.isFull());
        Assertions.assertFalse(transformed.isEmpty());
    }

    @Test
    void testTransform_infinite() {
        // arrange
        final TestLine line = TestLine.Y_AXIS;

        final StubRegion region = new StubRegion(Collections.singletonList(line.span()));

        final Transform<TestPoint2D> transform = new TestTransform2D(p -> new TestPoint2D(p.getX() + 1, p.getY() + 2));

        // act
        final StubRegion transformed = region.transform(transform);

        // assert
        final List<TestLineSegment> boundaries = transformed.getBoundaries();

        Assertions.assertEquals(1, boundaries.size());

        final TestLineSegment a = boundaries.get(0);
        final TestLine aLine = a.getHyperplane();
        PartitionTestUtils.assertPointsEqual(aLine.getOrigin(), new TestPoint2D(1, 0));
        Assertions.assertEquals(0.0, aLine.getDirectionX(), PartitionTestUtils.EPS);
        Assertions.assertEquals(1.0, aLine.getDirectionY(), PartitionTestUtils.EPS);

        GeometryTestUtils.assertNegativeInfinity(a.getStart());
        GeometryTestUtils.assertPositiveInfinity(a.getEnd());
    }

    @Test
    void testTransform_finite() {
        // arrange
        final TestPoint2D p1 = new TestPoint2D(1, 0);
        final TestPoint2D p2 = new TestPoint2D(2, 0);
        final TestPoint2D p3 = new TestPoint2D(1, 1);

        final StubRegion region = new StubRegion(Arrays.asList(
                new TestLineSegment(p1, p2),
                new TestLineSegment(p2, p3),
                new TestLineSegment(p3, p1)
            ));

        final Transform<TestPoint2D> transform = new TestTransform2D(p -> new TestPoint2D(p.getX() + 1, p.getY() + 2));

        // act
        final StubRegion transformed = region.transform(transform);

        // assert
        final List<TestLineSegment> boundaries = transformed.getBoundaries();

        Assertions.assertEquals(3, boundaries.size());

        final TestLineSegment a = boundaries.get(0);
        PartitionTestUtils.assertPointsEqual(new TestPoint2D(2, 2), a.getStartPoint());
        PartitionTestUtils.assertPointsEqual(new TestPoint2D(3, 2), a.getEndPoint());

        final TestLineSegment b = boundaries.get(1);
        PartitionTestUtils.assertPointsEqual(new TestPoint2D(3, 2), b.getStartPoint());
        PartitionTestUtils.assertPointsEqual(new TestPoint2D(2, 3), b.getEndPoint());

        final TestLineSegment c = boundaries.get(2);
        PartitionTestUtils.assertPointsEqual(new TestPoint2D(2, 3), c.getStartPoint());
        PartitionTestUtils.assertPointsEqual(new TestPoint2D(2, 2), c.getEndPoint());
    }

    @Test
    void testTransform_reflection() {
        // arrange
        final TestPoint2D p1 = new TestPoint2D(1, 0);
        final TestPoint2D p2 = new TestPoint2D(2, 0);
        final TestPoint2D p3 = new TestPoint2D(1, 1);

        final StubRegion region = new StubRegion(Arrays.asList(
                new TestLineSegment(p1, p2),
                new TestLineSegment(p2, p3),
                new TestLineSegment(p3, p1)
            ));

        final Transform<TestPoint2D> transform = new TestTransform2D(p -> new TestPoint2D(-p.getX(), p.getY()));

        // act
        final StubRegion transformed = region.transform(transform);

        // assert
        final List<TestLineSegment> boundaries = transformed.getBoundaries();

        Assertions.assertEquals(3, boundaries.size());

        final TestLineSegment a = boundaries.get(0);
        PartitionTestUtils.assertPointsEqual(new TestPoint2D(-2, 0), a.getStartPoint());
        PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 0), a.getEndPoint());

        final TestLineSegment b = boundaries.get(1);
        PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 1), b.getStartPoint());
        PartitionTestUtils.assertPointsEqual(new TestPoint2D(-2, 0), b.getEndPoint());

        final TestLineSegment c = boundaries.get(2);
        PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 0), c.getStartPoint());
        PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 1), c.getEndPoint());
    }

    @Test
    void testConvexRegionBoundaryBuilder_full() {
        // act
        final StubRegion region = StubRegion.fromBounds(Collections.emptyList());

        // assert
        Assertions.assertSame(StubRegion.FULL, region);
    }

    @Test
    void testConvexRegionBoundaryBuilder_singleLine() {
        // act
        final StubRegion region = StubRegion.fromBounds(Collections.singletonList(TestLine.Y_AXIS));

        // assert
        Assertions.assertEquals(1, region.getBoundaries().size());

        checkClassify(region, RegionLocation.INSIDE, new TestPoint2D(-1, 0));
        checkClassify(region, RegionLocation.BOUNDARY, new TestPoint2D(0, 0));
        checkClassify(region, RegionLocation.OUTSIDE, new TestPoint2D(1, 0));
    }

    @Test
    void testConvexRegionBoundaryBuilder_multipleLines() {
        // act
        final StubRegion region = StubRegion.fromBounds(Arrays.asList(
                    TestLine.X_AXIS,
                    new TestLine(new TestPoint2D(1, 0), new TestPoint2D(0, 1)),
                    TestLine.Y_AXIS.reverse()
                ));

        // assert
        Assertions.assertEquals(3, region.getBoundaries().size());

        checkClassify(region, RegionLocation.INSIDE, new TestPoint2D(0.25, 0.25));

        checkClassify(region, RegionLocation.BOUNDARY,
                TestPoint2D.ZERO, new TestPoint2D(1, 0), new TestPoint2D(1, 0), new TestPoint2D(0.5, 0.5));

        checkClassify(region, RegionLocation.OUTSIDE,
                new TestPoint2D(-1, 0.5), new TestPoint2D(1, 0.5),
                new TestPoint2D(0.5, 1), new TestPoint2D(0.5, -1));
    }

    @Test
    void testConvexRegionBoundaryBuilder_duplicateLines() {
        // act
        final StubRegion region = StubRegion.fromBounds(Arrays.asList(
                TestLine.Y_AXIS,
                TestLine.Y_AXIS,
                new TestLine(new TestPoint2D(0, 0), new TestPoint2D(0, 1)),
                TestLine.Y_AXIS));

        // assert
        Assertions.assertEquals(1, region.getBoundaries().size());

        checkClassify(region, RegionLocation.INSIDE, new TestPoint2D(-1, 0));
        checkClassify(region, RegionLocation.BOUNDARY, new TestPoint2D(0, 0));
        checkClassify(region, RegionLocation.OUTSIDE, new TestPoint2D(1, 0));
    }

    @Test
    void testConvexRegionBoundaryBuilder() {
        // arrange
        final List<TestLine> opposites = Arrays.asList(TestLine.X_AXIS, TestLine.X_AXIS.reverse());
        final List<TestLine> nonConvex = Arrays.asList(
                TestLine.X_AXIS,
                TestLine.Y_AXIS,
                new TestLine(new TestPoint2D(1, 0), new TestPoint2D(0, -1)),
                new TestLine(new TestPoint2D(1, 0), new TestPoint2D(0, -2)));

        // act/assert
        Assertions.assertThrows(IllegalArgumentException.class, () -> StubRegion.fromBounds(opposites));
        Assertions.assertThrows(IllegalArgumentException.class, () -> StubRegion.fromBounds(nonConvex));
    }

    @Test
    void testToString() {
        // arrange
        final StubRegion region = new StubRegion(Collections.emptyList());

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

        // assert
        Assertions.assertEquals("StubRegion[boundaries= []]", str);
    }

    private static void checkClassify(final Region<TestPoint2D> region, final RegionLocation loc, final TestPoint2D... pts) {
        for (final TestPoint2D pt : pts) {
            Assertions.assertEquals(loc, region.classify(pt), "Unexpected location for point " + pt);
        }
    }

    private static final class StubRegion extends AbstractConvexHyperplaneBoundedRegion<TestPoint2D, TestLineSegment> {

        private static final StubRegion FULL = new StubRegion(Collections.emptyList());

        StubRegion(final List<TestLineSegment> boundaries) {
            super(boundaries);
        }

        public StubRegion transform(final Transform<TestPoint2D> transform) {
            return transformInternal(transform, this, TestLineSegment.class, StubRegion::new);
        }

        @Override
        public Split<StubRegion> split(final Hyperplane<TestPoint2D> splitter) {
            return splitInternal(splitter, this, TestLineSegment.class, StubRegion::new);
        }

        @Override
        public TestLineSegment trim(final HyperplaneConvexSubset<TestPoint2D> subset) {
            return (TestLineSegment) super.trim(subset);
        }

        @Override
        public double getSize() {
            throw new UnsupportedOperationException();
        }

        @Override
        public TestPoint2D getCentroid() {
            throw new UnsupportedOperationException();
        }

        public static StubRegion fromBounds(final Iterable<TestLine> boundingLines) {
            final List<TestLineSegment> segments = new ConvexRegionBoundaryBuilder<>(TestLineSegment.class)
                    .build(boundingLines);
            return segments.isEmpty() ? FULL : new StubRegion(segments);
        }
    }
}