blob: 1ced9c1a3b888473406d7c3ba97c1c29593c6d36 [file] [log] [blame]
/*
* 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
*
* http://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.hull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.apache.commons.geometry.core.Region;
import org.apache.commons.geometry.core.RegionLocation;
import org.apache.commons.geometry.euclidean.twod.Vector2D;
import org.apache.commons.numbers.arrays.LinearCombination;
import org.apache.commons.numbers.core.Precision;
import org.apache.commons.rng.UniformRandomProvider;
import org.apache.commons.rng.simple.RandomSource;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
/**
* Abstract base test class for 2D convex hull generators.
*
*/
public abstract class ConvexHullGenerator2DAbstractTest {
protected ConvexHullGenerator2D generator;
protected UniformRandomProvider random;
protected abstract ConvexHullGenerator2D createConvexHullGenerator(boolean includeCollinearPoints);
protected Collection<Vector2D> reducePoints(Collection<Vector2D> points) {
// do nothing by default, may be overridden by other tests
return points;
}
@Before
public void setUp() {
// by default, do not include collinear points
generator = createConvexHullGenerator(false);
random = RandomSource.create(RandomSource.MT, 10);
}
// ------------------------------------------------------------------------------
@Test
public void testEmpty() {
ConvexHull2D hull = generator.generate(Collections.<Vector2D>emptyList());
Assert.assertTrue(hull.getVertices().length == 0);
Assert.assertTrue(hull.getLineSegments().length == 0);
}
@Test
public void testOnePoint() {
List<Vector2D> points = createRandomPoints(1);
ConvexHull2D hull = generator.generate(points);
Assert.assertTrue(hull.getVertices().length == 1);
Assert.assertTrue(hull.getLineSegments().length == 0);
}
@Test
public void testTwoPoints() {
List<Vector2D> points = createRandomPoints(2);
ConvexHull2D hull = generator.generate(points);
Assert.assertTrue(hull.getVertices().length == 2);
Assert.assertTrue(hull.getLineSegments().length == 1);
}
@Test
public void testAllIdentical() {
final Collection<Vector2D> points = new ArrayList<>();
points.add(Vector2D.of(1, 1));
points.add(Vector2D.of(1, 1));
points.add(Vector2D.of(1, 1));
points.add(Vector2D.of(1, 1));
final ConvexHull2D hull = generator.generate(points);
Assert.assertTrue(hull.getVertices().length == 1);
}
@Test
public void testConvexHull() {
// execute 100 random variations
for (int i = 0; i < 100; i++) {
// randomize the size from 4 to 100
int size = (int) Math.floor(random.nextDouble() * 96.0 + 4.0);
List<Vector2D> points = createRandomPoints(size);
ConvexHull2D hull = generator.generate(reducePoints(points));
checkConvexHull(points, hull);
}
}
@Test
public void testCollinearPoints() {
final Collection<Vector2D> points = new ArrayList<>();
points.add(Vector2D.of(1, 1));
points.add(Vector2D.of(2, 2));
points.add(Vector2D.of(2, 4));
points.add(Vector2D.of(4, 1));
points.add(Vector2D.of(10, 1));
final ConvexHull2D hull = generator.generate(points);
checkConvexHull(points, hull);
}
@Test
public void testCollinearPointsReverse() {
final Collection<Vector2D> points = new ArrayList<>();
points.add(Vector2D.of(1, 1));
points.add(Vector2D.of(2, 2));
points.add(Vector2D.of(2, 4));
points.add(Vector2D.of(10, 1));
points.add(Vector2D.of(4, 1));
final ConvexHull2D hull = generator.generate(points);
checkConvexHull(points, hull);
}
@Test
public void testCollinearPointsIncluded() {
final Collection<Vector2D> points = new ArrayList<>();
points.add(Vector2D.of(1, 1));
points.add(Vector2D.of(2, 2));
points.add(Vector2D.of(2, 4));
points.add(Vector2D.of(4, 1));
points.add(Vector2D.of(10, 1));
final ConvexHull2D hull = createConvexHullGenerator(true).generate(points);
checkConvexHull(points, hull, true);
}
@Test
public void testCollinearPointsIncludedReverse() {
final Collection<Vector2D> points = new ArrayList<>();
points.add(Vector2D.of(1, 1));
points.add(Vector2D.of(2, 2));
points.add(Vector2D.of(2, 4));
points.add(Vector2D.of(10, 1));
points.add(Vector2D.of(4, 1));
final ConvexHull2D hull = createConvexHullGenerator(true).generate(points);
checkConvexHull(points, hull, true);
}
@Test
public void testIdenticalPoints() {
final Collection<Vector2D> points = new ArrayList<>();
points.add(Vector2D.of(1, 1));
points.add(Vector2D.of(2, 2));
points.add(Vector2D.of(2, 4));
points.add(Vector2D.of(4, 1));
points.add(Vector2D.of(1, 1));
final ConvexHull2D hull = generator.generate(points);
checkConvexHull(points, hull);
}
@Test
public void testIdenticalPoints2() {
final Collection<Vector2D> points = new ArrayList<>();
points.add(Vector2D.of(1, 1));
points.add(Vector2D.of(2, 2));
points.add(Vector2D.of(2, 4));
points.add(Vector2D.of(4, 1));
points.add(Vector2D.of(1, 1));
final ConvexHull2D hull = createConvexHullGenerator(true).generate(points);
checkConvexHull(points, hull, true);
}
@Test
public void testClosePoints() {
final Collection<Vector2D> points = new ArrayList<>();
points.add(Vector2D.of(1, 1));
points.add(Vector2D.of(2, 2));
points.add(Vector2D.of(2, 4));
points.add(Vector2D.of(4, 1));
points.add(Vector2D.of(1.00001, 1));
final ConvexHull2D hull = generator.generate(points);
checkConvexHull(points, hull);
}
@Test
public void testCollinearPointOnExistingBoundary() {
// MATH-1135: check that collinear points on the hull are handled correctly
// when only a minimal hull shall be constructed
final Collection<Vector2D> points = new ArrayList<>();
points.add(Vector2D.of(7.3152, 34.7472));
points.add(Vector2D.of(6.400799999999997, 34.747199999999985));
points.add(Vector2D.of(5.486399999999997, 34.7472));
points.add(Vector2D.of(4.876799999999999, 34.7472));
points.add(Vector2D.of(4.876799999999999, 34.1376));
points.add(Vector2D.of(4.876799999999999, 30.48));
points.add(Vector2D.of(6.0959999999999965, 30.48));
points.add(Vector2D.of(6.0959999999999965, 34.1376));
points.add(Vector2D.of(7.315199999999996, 34.1376));
points.add(Vector2D.of(7.3152, 30.48));
final ConvexHull2D hull = createConvexHullGenerator(false).generate(points);
checkConvexHull(points, hull);
}
@Test
public void testCollinearPointsInAnyOrder() {
// MATH-1148: collinear points on the hull might be in any order
// make sure that they are processed in the proper order
// for each algorithm.
List<Vector2D> points = new ArrayList<>();
// first case: 3 points are collinear
points.add(Vector2D.of(16.078200000000184, -36.52519999989808));
points.add(Vector2D.of(19.164300000000186, -36.52519999989808));
points.add(Vector2D.of(19.1643, -25.28136477910407));
points.add(Vector2D.of(19.1643, -17.678400000004157));
ConvexHull2D hull = createConvexHullGenerator(false).generate(points);
checkConvexHull(points, hull);
hull = createConvexHullGenerator(true).generate(points);
checkConvexHull(points, hull, true);
points.clear();
// second case: multiple points are collinear
points.add(Vector2D.of(0, -29.959696875));
points.add(Vector2D.of(0, -31.621809375));
points.add(Vector2D.of(0, -28.435696875));
points.add(Vector2D.of(0, -33.145809375));
points.add(Vector2D.of(3.048, -33.145809375));
points.add(Vector2D.of(3.048, -31.621809375));
points.add(Vector2D.of(3.048, -29.959696875));
points.add(Vector2D.of(4.572, -33.145809375));
points.add(Vector2D.of(4.572, -28.435696875));
hull = createConvexHullGenerator(false).generate(points);
checkConvexHull(points, hull);
hull = createConvexHullGenerator(true).generate(points);
checkConvexHull(points, hull, true);
}
@Test
public void testIssue1123() {
List<Vector2D> points = new ArrayList<>();
int[][] data = new int[][] { { -11, -1 }, { -11, 0 }, { -11, 1 },
{ -10, -3 }, { -10, -2 }, { -10, -1 }, { -10, 0 }, { -10, 1 },
{ -10, 2 }, { -10, 3 }, { -9, -4 }, { -9, -3 }, { -9, -2 },
{ -9, -1 }, { -9, 0 }, { -9, 1 }, { -9, 2 }, { -9, 3 },
{ -9, 4 }, { -8, -5 }, { -8, -4 }, { -8, -3 }, { -8, -2 },
{ -8, -1 }, { -8, 0 }, { -8, 1 }, { -8, 2 }, { -8, 3 },
{ -8, 4 }, { -8, 5 }, { -7, -6 }, { -7, -5 }, { -7, -4 },
{ -7, -3 }, { -7, -2 }, { -7, -1 }, { -7, 0 }, { -7, 1 },
{ -7, 2 }, { -7, 3 }, { -7, 4 }, { -7, 5 }, { -7, 6 },
{ -6, -7 }, { -6, -6 }, { -6, -5 }, { -6, -4 }, { -6, -3 },
{ -6, -2 }, { -6, -1 }, { -6, 0 }, { -6, 1 }, { -6, 2 },
{ -6, 3 }, { -6, 4 }, { -6, 5 }, { -6, 6 }, { -6, 7 },
{ -5, -7 }, { -5, -6 }, { -5, -5 }, { -5, -4 }, { -5, -3 },
{ -5, -2 }, { -5, 4 }, { -5, 5 }, { -5, 6 }, { -5, 7 },
{ -4, -7 }, { -4, -6 }, { -4, -5 }, { -4, -4 }, { -4, -3 },
{ -4, -2 }, { -4, 4 }, { -4, 5 }, { -4, 6 }, { -4, 7 },
{ -3, -8 }, { -3, -7 }, { -3, -6 }, { -3, -5 }, { -3, -4 },
{ -3, -3 }, { -3, -2 }, { -3, 4 }, { -3, 5 }, { -3, 6 },
{ -3, 7 }, { -3, 8 }, { -2, -8 }, { -2, -7 }, { -2, -6 },
{ -2, -5 }, { -2, -4 }, { -2, -3 }, { -2, -2 }, { -2, 4 },
{ -2, 5 }, { -2, 6 }, { -2, 7 }, { -2, 8 }, { -1, -8 },
{ -1, -7 }, { -1, -6 }, { -1, -5 }, { -1, -4 }, { -1, -3 },
{ -1, -2 }, { -1, 4 }, { -1, 5 }, { -1, 6 }, { -1, 7 },
{ -1, 8 }, { 0, -8 }, { 0, -7 }, { 0, -6 }, { 0, -5 },
{ 0, -4 }, { 0, -3 }, { 0, -2 }, { 0, 4 }, { 0, 5 }, { 0, 6 },
{ 0, 7 }, { 0, 8 }, { 1, -8 }, { 1, -7 }, { 1, -6 }, { 1, -5 },
{ 1, -4 }, { 1, -3 }, { 1, -2 }, { 1, -1 }, { 1, 0 }, { 1, 1 },
{ 1, 2 }, { 1, 3 }, { 1, 4 }, { 1, 5 }, { 1, 6 }, { 1, 7 },
{ 1, 8 }, { 2, -8 }, { 2, -7 }, { 2, -6 }, { 2, -5 },
{ 2, -4 }, { 2, -3 }, { 2, -2 }, { 2, -1 }, { 2, 0 }, { 2, 1 },
{ 2, 2 }, { 2, 3 }, { 2, 4 }, { 2, 5 }, { 2, 6 }, { 2, 7 },
{ 2, 8 }, { 3, -8 }, { 3, -7 }, { 3, -6 }, { 3, -5 },
{ 3, -4 }, { 3, -3 }, { 3, -2 }, { 3, -1 }, { 3, 0 }, { 3, 1 },
{ 3, 2 }, { 3, 3 }, { 3, 4 }, { 3, 5 }, { 3, 6 }, { 3, 7 },
{ 3, 8 }, { 4, -7 }, { 4, -6 }, { 4, -5 }, { 4, -4 },
{ 4, -3 }, { 4, -2 }, { 4, -1 }, { 4, 0 }, { 4, 1 }, { 4, 2 },
{ 4, 3 }, { 4, 4 }, { 4, 5 }, { 4, 6 }, { 4, 7 }, { 5, -7 },
{ 5, -6 }, { 5, -5 }, { 5, -4 }, { 5, -3 }, { 5, -2 },
{ 5, -1 }, { 5, 0 }, { 5, 1 }, { 5, 2 }, { 5, 3 }, { 5, 4 },
{ 5, 5 }, { 5, 6 }, { 5, 7 }, { 6, -7 }, { 6, -6 }, { 6, -5 },
{ 6, -4 }, { 6, -3 }, { 6, -2 }, { 6, -1 }, { 6, 0 }, { 6, 1 },
{ 6, 2 }, { 6, 3 }, { 6, 4 }, { 6, 5 }, { 6, 6 }, { 6, 7 },
{ 7, -6 }, { 7, -5 }, { 7, -4 }, { 7, -3 }, { 7, -2 },
{ 7, -1 }, { 7, 0 }, { 7, 1 }, { 7, 2 }, { 7, 3 }, { 7, 4 },
{ 7, 5 }, { 7, 6 }, { 8, -5 }, { 8, -4 }, { 8, -3 }, { 8, -2 },
{ 8, -1 }, { 8, 0 }, { 8, 1 }, { 8, 2 }, { 8, 3 }, { 8, 4 },
{ 8, 5 }, { 9, -4 }, { 9, -3 }, { 9, -2 }, { 9, -1 }, { 9, 0 },
{ 9, 1 }, { 9, 2 }, { 9, 3 }, { 9, 4 }, { 10, -3 }, { 10, -2 },
{ 10, -1 }, { 10, 0 }, { 10, 1 }, { 10, 2 }, { 10, 3 },
{ 11, -1 }, { 11, 0 }, { 11, 1 } };
for (int[] line : data) {
points.add(Vector2D.of(line[0], line[1]));
}
Vector2D[] referenceHull = new Vector2D[] {
Vector2D.of(-11.0, -1.0),
Vector2D.of(-10.0, -3.0),
Vector2D.of( -6.0, -7.0),
Vector2D.of( -3.0, -8.0),
Vector2D.of( 3.0, -8.0),
Vector2D.of( 6.0, -7.0),
Vector2D.of( 10.0, -3.0),
Vector2D.of( 11.0, -1.0),
Vector2D.of( 11.0, 1.0),
Vector2D.of( 10.0, 3.0),
Vector2D.of( 6.0, 7.0),
Vector2D.of( 3.0, 8.0),
Vector2D.of( -3.0, 8.0),
Vector2D.of( -6.0, 7.0),
Vector2D.of(-10.0, 3.0),
Vector2D.of(-11.0, 1.0),
};
ConvexHull2D convHull = generator.generate(points);
Region<Vector2D> hullRegion = convHull.createRegion();
Assert.assertEquals(274.0, hullRegion.getSize(), 1.0e-12);
double perimeter = 0;
for (int i = 0; i < referenceHull.length; ++i) {
perimeter += referenceHull[i].distance(
referenceHull[(i + 1) % referenceHull.length]);
}
Assert.assertEquals(perimeter, hullRegion.getBoundarySize(), 1.0e-12);
for (int i = 0; i < referenceHull.length; ++i) {
Assert.assertEquals(RegionLocation.BOUNDARY, hullRegion.classify(referenceHull[i]));
}
}
// ------------------------------------------------------------------------------
protected final List<Vector2D> createRandomPoints(int size) {
// create the cloud container
List<Vector2D> points = new ArrayList<>(size);
// fill the cloud with a random distribution of points
for (int i = 0; i < size; i++) {
points.add(Vector2D.of(random.nextDouble() * 2.0 - 1.0, random.nextDouble() * 2.0 - 1.0));
}
return points;
}
protected final void checkConvexHull(final Collection<Vector2D> points, final ConvexHull2D hull) {
checkConvexHull(points, hull, false);
}
protected final void checkConvexHull(final Collection<Vector2D> points, final ConvexHull2D hull,
final boolean includesCollinearPoints) {
checkConvexHull(points, hull, includesCollinearPoints, 1e-10);
}
protected final void checkConvexHull(final Collection<Vector2D> points, final ConvexHull2D hull,
final boolean includesCollinearPoints, final double tolerance) {
Assert.assertNotNull(hull);
Assert.assertTrue(isConvex(hull, includesCollinearPoints, tolerance));
checkPointsInsideHullRegion(points, hull, includesCollinearPoints);
}
// verify that the constructed hull is really convex
protected final boolean isConvex(final ConvexHull2D hull, final boolean includesCollinearPoints,
final double tolerance) {
final Vector2D[] points = hull.getVertices();
int sign = 0;
for (int i = 0; i < points.length; i++) {
Vector2D p1 = points[i == 0 ? points.length - 1 : i - 1];
Vector2D p2 = points[i];
Vector2D p3 = points[i == points.length - 1 ? 0 : i + 1];
Vector2D d1 = p2.subtract(p1);
Vector2D d2 = p3.subtract(p2);
Assert.assertTrue(d1.norm() > 1e-10);
Assert.assertTrue(d2.norm() > 1e-10);
final double cross = LinearCombination.value(d1.getX(), d2.getY(), -d1.getY(), d2.getX());
final int cmp = Precision.compareTo(cross, 0.0, tolerance);
if (sign != 0 && cmp != sign) {
if (includesCollinearPoints && cmp == 0) {
// in case of collinear points the cross product will be zero
} else {
return false;
}
}
sign = cmp;
}
return true;
}
// verify that all points are inside the convex hull region
protected final void checkPointsInsideHullRegion(final Collection<Vector2D> points,
final ConvexHull2D hull,
final boolean includesCollinearPoints) {
final Collection<Vector2D> hullVertices = Arrays.asList(hull.getVertices());
final Region<Vector2D> region = hull.createRegion();
for (final Vector2D p : points) {
RegionLocation location = region.classify(p);
Assert.assertTrue(location != RegionLocation.OUTSIDE);
if (location == RegionLocation.BOUNDARY && includesCollinearPoints) {
Assert.assertTrue(hullVertices.contains(p));
}
}
}
}