blob: e1efb4b89dfad4a4389aae886bb8c9effd077446 [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.core.partitioning;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import org.apache.commons.geometry.core.Point;
import org.apache.commons.geometry.core.RegionLocation;
import org.apache.commons.geometry.core.Transform;
import org.apache.commons.geometry.core.exception.GeometryException;
/** Base class for convex hyperplane-bounded regions. This class provides generic implementations of many
* algorithms related to convex regions.
* @param <P> Point implementation type
* @param <S> Convex subhyperplane implementation type
*/
public abstract class AbstractConvexHyperplaneBoundedRegion<P extends Point<P>, S extends ConvexSubHyperplane<P>>
implements HyperplaneBoundedRegion<P> {
/** List of boundaries for the region. */
private final List<S> boundaries;
/** Simple constructor. Callers are responsible for ensuring that the given list of subhyperplanes
* represent a valid convex region boundary. No validation is performed.
* @param boundaries the boundaries of the convex region
*/
protected AbstractConvexHyperplaneBoundedRegion(final List<S> boundaries) {
this.boundaries = Collections.unmodifiableList(boundaries);
}
/** Get the boundaries of the convex region. The exact ordering of the boundaries
* is not guaranteed.
* @return the boundaries of the convex region
*/
public List<S> getBoundaries() {
return boundaries;
}
/** {@inheritDoc} */
@Override
public boolean isFull() {
// no boundaries => no outside
return boundaries.isEmpty();
}
/** {@inheritDoc}
*
* <p>This method always returns false.</p>
*/
@Override
public boolean isEmpty() {
return false;
}
/** {@inheritDoc} */
@Override
public double getBoundarySize() {
double sum = 0.0;
for (final S boundary : boundaries) {
sum += boundary.getSize();
}
return sum;
}
/** {@inheritDoc} */
@Override
public RegionLocation classify(P pt) {
boolean isOn = false;
HyperplaneLocation loc;
for (final S boundary : boundaries) {
loc = boundary.getHyperplane().classify(pt);
if (loc == HyperplaneLocation.PLUS) {
return RegionLocation.OUTSIDE;
} else if (loc == HyperplaneLocation.ON) {
isOn = true;
}
}
return isOn ? RegionLocation.BOUNDARY : RegionLocation.INSIDE;
}
/** {@inheritDoc} */
@Override
public P project(P pt) {
P projected;
double dist;
P closestPt = null;
double closestDist = Double.POSITIVE_INFINITY;
for (final S boundary : boundaries) {
projected = boundary.closest(pt);
dist = pt.distance(projected);
if (projected != null && (closestPt == null || dist < closestDist)) {
closestPt = projected;
closestDist = dist;
}
}
return closestPt;
}
/** Trim the given convex subhyperplane to the portion contained inside this instance.
* @param convexSubHyperplane convex subhyperplane to trim. Null is returned if the subhyperplane
* does not intersect the instance.
* @return portion of the argument that lies entirely inside the region represented by
* this instance, or null if it does not intersect.
*/
public ConvexSubHyperplane<P> trim(final ConvexSubHyperplane<P> convexSubHyperplane) {
ConvexSubHyperplane<P> remaining = convexSubHyperplane;
for (final S boundary : boundaries) {
remaining = remaining.split(boundary.getHyperplane()).getMinus();
if (remaining == null) {
break;
}
}
return remaining;
}
/** {@inheritDoc} */
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append(this.getClass().getSimpleName())
.append("[boundaries= ")
.append(boundaries);
return sb.toString();
}
/** Generic, internal transform method. Subclasses should use this to implement their own transform methods.
* @param transform the transform to apply to the instance
* @param thisInstance a reference to the current instance; this is passed as
* an argument in order to allow it to be a generic type
* @param subhpType the type used for the boundary subhyperplanes
* @param factory function used to create new convex region instances
* @param <R> Region implementation type
* @return the result of the transform operation
*/
protected <R extends AbstractConvexHyperplaneBoundedRegion<P, S>> R transformInternal(
final Transform<P> transform, final R thisInstance, final Class<S> subhpType,
final Function<List<S>, R> factory) {
if (isFull()) {
return thisInstance;
}
final List<S> origBoundaries = getBoundaries();
final int size = origBoundaries.size();
final List<S> tBoundaries = new ArrayList<>(size);
// determine if the hyperplanes should be reversed
final S boundary = origBoundaries.get(0);
ConvexSubHyperplane<P> tBoundary = boundary.transform(transform);
final boolean reverseDirection = swapsInsideOutside(transform);
// transform all of the segments
if (reverseDirection) {
tBoundary = tBoundary.reverse();
}
tBoundaries.add(subhpType.cast(tBoundary));
for (int i = 1; i < origBoundaries.size(); ++i) {
tBoundary = origBoundaries.get(i).transform(transform);
if (reverseDirection) {
tBoundary = tBoundary.reverse();
}
tBoundaries.add(subhpType.cast(tBoundary));
}
return factory.apply(tBoundaries);
}
/** Return true if the given transform swaps the inside and outside of
* the region.
*
* <p>The default behavior of this method is to return true if the transform
* does not preserve spatial orientation (ie, {@link Transform#preservesOrientation()}
* is false). Subclasses may need to override this method to implement the correct
* behavior for their space and dimension.</p>
* @param transform transform to check
* @return true if the given transform swaps the interior and exterior of
* the region
*/
protected boolean swapsInsideOutside(final Transform<P> transform) {
return !transform.preservesOrientation();
}
/** Generic, internal split method. Subclasses should call this from their {@link #split(Hyperplane)} methods.
* @param splitter splitting hyperplane
* @param thisInstance a reference to the current instance; this is passed as
* an argument in order to allow it to be a generic type
* @param subhpType the type used for the boundary subhyperplanes
* @param factory function used to create new convex region instances
* @param <R> Region implementation type
* @return the result of the split operation
*/
protected <R extends AbstractConvexHyperplaneBoundedRegion<P, S>> Split<R> splitInternal(
final Hyperplane<P> splitter, final R thisInstance, final Class<S> subhpType,
final Function<List<S>, R> factory) {
if (isFull()) {
final R minus = factory.apply(Arrays.asList(subhpType.cast(splitter.span())));
final R plus = factory.apply(Arrays.asList(subhpType.cast(splitter.reverse().span())));
return new Split<>(minus, plus);
} else {
final ConvexSubHyperplane<P> trimmedSplitter = trim(splitter.span());
if (trimmedSplitter == null) {
// The splitter lies entirely outside of the region; we need
// to determine whether we lie on the plus or minus side of the splitter.
// We can use the first boundary to determine this. If the boundary is entirely
// on the minus side of the splitter or lies directly on the splitter and has
// the same orientation, then the area lies on the minus side of the splitter.
// Otherwise, it lies on the plus side.
final ConvexSubHyperplane<P> testSegment = boundaries.get(0);
final SplitLocation testLocation = testSegment.split(splitter).getLocation();
if (SplitLocation.MINUS == testLocation ||
(SplitLocation.NEITHER == testLocation &&
splitter.similarOrientation(testSegment.getHyperplane()))) {
return new Split<>(thisInstance, null);
}
return new Split<>(null, thisInstance);
}
final List<S> minusBoundaries = new ArrayList<>();
final List<S> plusBoundaries = new ArrayList<>();
Split<? extends ConvexSubHyperplane<P>> split;
ConvexSubHyperplane<P> minusBoundary;
ConvexSubHyperplane<P> plusBoundary;
for (final S boundary : boundaries) {
split = boundary.split(splitter);
minusBoundary = split.getMinus();
plusBoundary = split.getPlus();
if (minusBoundary != null) {
minusBoundaries.add(subhpType.cast(minusBoundary));
}
if (plusBoundary != null) {
plusBoundaries.add(subhpType.cast(plusBoundary));
}
}
minusBoundaries.add(subhpType.cast(trimmedSplitter));
plusBoundaries.add(subhpType.cast(trimmedSplitter.reverse()));
return new Split<>(factory.apply(minusBoundaries), factory.apply(plusBoundaries));
}
}
/** Internal class encapsulating the logic for building convex region boundaries from collections of
* hyperplanes.
* @param <P> Point implementation type
* @param <S> ConvexSubHyperplane implementation type
*/
protected static class ConvexRegionBoundaryBuilder<P extends Point<P>, S extends ConvexSubHyperplane<P>> {
/** Convex subhyperplane implementation type. */
private final Class<S> subhyperplaneType;
/** Construct a new instance for building convex region boundaries with the given convex subhyperplane
* implementation type.
* @param subhyperplaneType Convex subhyperplane implementation type
*/
public ConvexRegionBoundaryBuilder(final Class<S> subhyperplaneType) {
this.subhyperplaneType = subhyperplaneType;
}
/** Compute a list of convex subhyperplanes representing the boundaries of the convex region
* bounded by the given collection of hyperplanes.
* @param bounds hyperplanes defining the convex region
* @return a list of convex subhyperplanes representing the boundaries of the convex region
* @throws GeometryException if the given hyperplanes do not form a convex region
*/
public List<S> build(final Iterable<? extends Hyperplane<P>> bounds) {
final List<S> boundaries = new ArrayList<>();
// cut each hyperplane by every other hyperplane in order to get the subplane boundaries
boolean notConvex = false;
int outerIdx = 0;
ConvexSubHyperplane<P> subhp;
for (final Hyperplane<P> hp : bounds) {
++outerIdx;
subhp = hp.span();
int innerIdx = 0;
for (final Hyperplane<P> splitter : bounds) {
++innerIdx;
if (hp != splitter) {
final Split<? extends ConvexSubHyperplane<P>> split = subhp.split(splitter);
if (split.getLocation() == SplitLocation.NEITHER) {
if (hp.similarOrientation(splitter)) {
// two or more splitters are the equivalent; only
// use the segment from the first one
if (outerIdx > innerIdx) {
subhp = null;
}
} else {
// two or more splitters are coincident and have opposite
// orientations, meaning that no area is on the minus side
// of both
notConvex = true;
break;
}
} else {
subhp = subhp.split(splitter).getMinus();
}
if (subhp == null) {
break;
}
} else if (outerIdx > innerIdx) {
// this hyperplane is duplicated in the list; skip all but the
// first insertion of its subhyperplane
subhp = null;
break;
}
}
if (notConvex) {
break;
}
if (subhp != null) {
boundaries.add(subhyperplaneType.cast(subhp));
}
}
if (notConvex || (outerIdx > 0 && boundaries.isEmpty())) {
throw new GeometryException("Bounding hyperplanes do not produce a convex region: " + bounds);
}
return boundaries;
}
}
}