blob: 15bd465d1ef778f664d46281b2b4506fb4e4702f [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.spherical.oned;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import org.apache.commons.geometry.core.Geometry;
import org.apache.commons.geometry.core.Transform;
import org.apache.commons.geometry.core.partitioning.Hyperplane;
import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
import org.apache.commons.geometry.core.partitioning.Split;
import org.apache.commons.geometry.core.partitioning.SubHyperplane;
import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
/** BSP tree representing regions in 1D spherical space.
*/
public class RegionBSPTree1S extends AbstractRegionBSPTree<Point1S, RegionBSPTree1S.RegionNode1S> {
/** Comparator used to sort BoundaryPairs by ascending azimuth. */
private static final Comparator<BoundaryPair> BOUNDARY_PAIR_COMPARATOR = (BoundaryPair a, BoundaryPair b) -> {
return Double.compare(a.getMinValue(), b.getMinValue());
};
/** Create a new, empty instance.
*/
public RegionBSPTree1S() {
this(false);
}
/** Create a new region. If {@code full} is true, then the region will
* represent the entire circle. Otherwise, it will be empty.
* @param full whether or not the region should contain the entire
* circle or be empty
*/
public RegionBSPTree1S(boolean full) {
super(full);
}
/** Return a deep copy of this instance.
* @return a deep copy of this instance.
* @see #copy(org.apache.commons.geometry.core.partitioning.bsp.BSPTree)
*/
public RegionBSPTree1S copy() {
RegionBSPTree1S result = RegionBSPTree1S.empty();
result.copy(this);
return result;
}
/** Add an interval to this region. The resulting region will be the
* union of the interval and the region represented by this instance.
* @param interval the interval to add
*/
public void add(final AngularInterval interval) {
union(fromInterval(interval));
}
/** {@inheritDoc} */
@Override
public Point1S project(final Point1S pt) {
final BoundaryProjector1S projector = new BoundaryProjector1S(pt);
accept(projector);
return projector.getProjected();
}
/** {@inheritDoc}
*
* <p>Each interval of the region is transformed individually and the
* results are unioned. If the size of any transformed interval is greater
* than or equal to 2pi, then the region is set to the full space.</p>
*/
@Override
public void transform(final Transform<Point1S> transform) {
if (!isFull() && !isEmpty()) {
// transform each interval individually to handle wrap-around
final List<AngularInterval> intervals = toIntervals();
setEmpty();
for (AngularInterval interval : intervals) {
union(interval.transform(transform).toTree());
}
}
}
/** {@inheritDoc}
*
* <p>It is important to note that split operations occur according to the rules of the
* {@link CutAngle} hyperplane class. In this class, the continuous circle is viewed
* as a non-circular segment of the number line in the range {@code [0, 2pi)}. Hyperplanes
* are placed along this line and partition it into the segments {@code [0, x]}
* and {@code [x, 2pi)}, where {@code x} is the location of the hyperplane. For example,
* a positive-facing {@link CutAngle} instance with an azimuth of {@code 0.5pi} has
* a minus side consisting of the angles {@code [0, 0.5pi]} and a plus side consisting of
* the angles {@code [0.5pi, 2pi)}. Similarly, a positive-facing {@link CutAngle} with
* an azimuth of {@code 0pi} has a plus side of {@code [0, 2pi)} (the full space) and
* a minus side that is completely empty (since no points exist in our domain that are
* less than zero). These rules can result in somewhat non-intuitive behavior at times.
* For example, splitting a non-empty region with a hyperplane at {@code 0pi} is
* essentially a no-op, since the region will either lie entirely on the plus or minus
* side of the hyperplane (depending on the hyperplane's orientation) regardless of the actual
* content of the region. In these situations, a copy of the tree is returned on the
* appropriate side of the split.</p>
*
* @see CutAngle
* @see #splitDiameter(CutAngle)
*/
@Override
public Split<RegionBSPTree1S> split(final Hyperplane<Point1S> splitter) {
// Handle the special case where the cut is on the azimuth equivalent to zero;
// In this case, it is not possible for any points to lie between it and zero.
if (!isEmpty() && splitter.classify(Point1S.ZERO) == HyperplaneLocation.ON) {
CutAngle cut = (CutAngle) splitter;
if (cut.isPositiveFacing()) {
return new Split<>(null, copy());
} else {
return new Split<>(copy(), null);
}
}
return split(splitter, RegionBSPTree1S.empty(), RegionBSPTree1S.empty());
}
/** Split the instance along a circle diameter.The diameter is defined by the given
* split point and its reversed antipodal point.
* @param splitter split point defining one side of the split diameter
* @return result of the split operation
*/
public Split<RegionBSPTree1S> splitDiameter(final CutAngle splitter) {
final CutAngle opposite = CutAngle.fromPointAndDirection(
splitter.getPoint().antipodal(),
!splitter.isPositiveFacing(),
splitter.getPrecision());
final double plusPoleOffset = splitter.isPositiveFacing() ?
+Geometry.HALF_PI :
-Geometry.HALF_PI;
final Point1S plusPole = Point1S.of(splitter.getAzimuth() + plusPoleOffset);
final boolean zeroOnPlusSide = splitter.getPrecision()
.lte(plusPole.distance(Point1S.ZERO), Geometry.HALF_PI);
Split<RegionBSPTree1S> firstSplit = split(splitter);
Split<RegionBSPTree1S> secondSplit = split(opposite);
RegionBSPTree1S minus = RegionBSPTree1S.empty();
RegionBSPTree1S plus = RegionBSPTree1S.empty();
if (zeroOnPlusSide) {
// zero wrap-around needs to be handled on the plus side of the split
safeUnion(plus, firstSplit.getPlus());
safeUnion(plus, secondSplit.getPlus());
minus = firstSplit.getMinus();
if (minus != null) {
minus = minus.split(opposite).getMinus();
}
} else {
// zero wrap-around needs to be handled on the minus side of the split
safeUnion(minus, firstSplit.getMinus());
safeUnion(minus, secondSplit.getMinus());
plus = firstSplit.getPlus();
if (plus != null) {
plus = plus.split(opposite).getPlus();
}
}
return new Split<>(
(minus != null && !minus.isEmpty()) ? minus : null,
(plus != null && !plus.isEmpty()) ? plus : null);
}
/** Convert the region represented by this tree into a list of separate
* {@link AngularInterval}s, arranged in order of ascending min value.
* @return list of {@link AngularInterval}s representing this region in order of
* ascending min value
*/
public List<AngularInterval> toIntervals() {
if (isFull()) {
return Collections.singletonList(AngularInterval.full());
}
final List<BoundaryPair> insideBoundaryPairs = new ArrayList<>();
for (RegionNode1S node : this) {
if (node.isInside()) {
insideBoundaryPairs.add(getNodeBoundaryPair(node));
}
}
insideBoundaryPairs.sort(BOUNDARY_PAIR_COMPARATOR);
int boundaryPairCount = insideBoundaryPairs.size();
// Find the index of the first boundary pair that is not connected to pair before it.
// This will be our start point for merging intervals together.
int startOffset = 0;
if (boundaryPairCount > 1) {
BoundaryPair current = null;
BoundaryPair previous = insideBoundaryPairs.get(boundaryPairCount - 1);
for (int i = 0; i < boundaryPairCount; ++i, previous = current) {
current = insideBoundaryPairs.get(i);
if (!Objects.equals(current.getMin(), previous.getMax())) {
startOffset = i;
break;
}
}
}
// Go through the pairs starting at the start offset and create intervals
// for each set of adjacent pairs.
final List<AngularInterval> intervals = new ArrayList<>();
BoundaryPair start = null;
BoundaryPair end = null;
BoundaryPair current = null;
for (int i = 0; i < boundaryPairCount; ++i) {
current = insideBoundaryPairs.get((i + startOffset) % boundaryPairCount);
if (start == null) {
start = current;
end = current;
} else if (Objects.equals(end.getMax(), current.getMin())) {
// these intervals should be merged
end = current;
} else {
// these intervals should be separate
intervals.add(createInterval(start, end));
// queue up the next pair
start = current;
end = current;
}
}
if (start != null && end != null) {
intervals.add(createInterval(start, end));
}
return intervals;
}
/** Create an interval instance from the min boundary from the start boundary pair and
* the max boundary from the end boundary pair. The hyperplane directions are adjusted
* as needed.
* @param start starting boundary pair
* @param end ending boundary pair
* @return an interval created from the min boundary of the given start pair and the
* max boundary from the given end pair
*/
private AngularInterval createInterval(final BoundaryPair start, final BoundaryPair end) {
CutAngle min = start.getMin();
CutAngle max = end.getMax();
DoublePrecisionContext precision = (min != null) ? min.getPrecision() : max.getPrecision();
// flip the hyperplanes if needed since there's no
// guarantee that the inside will be on the minus side
// of the hyperplane (for example, if the region is complemented)
if (min != null) {
if (min.isPositiveFacing()) {
min = min.reverse();
}
} else {
min = CutAngle.createNegativeFacing(Geometry.ZERO_PI, precision);
}
if (max != null) {
if (!max.isPositiveFacing()) {
max = max.reverse();
}
} else {
max = CutAngle.createPositiveFacing(Geometry.TWO_PI, precision);
}
return AngularInterval.of(min, max);
}
/** Return the min/max boundary pair for the convex region represented by the given node.
* @param node the node to compute the interval for
* @return the min/max boundary pair for the convex region represented by the given node
*/
private BoundaryPair getNodeBoundaryPair(final RegionNode1S node) {
CutAngle min = null;
CutAngle max = null;
CutAngle pt;
RegionNode1S child = node;
RegionNode1S parent;
while ((min == null || max == null) && (parent = child.getParent()) != null) {
pt = (CutAngle) parent.getCutHyperplane();
if ((pt.isPositiveFacing() && child.isMinus()) ||
(!pt.isPositiveFacing() && child.isPlus())) {
if (max == null) {
max = pt;
}
} else if (min == null) {
min = pt;
}
child = parent;
}
return new BoundaryPair(min, max);
}
/** {@inheritDoc} */
@Override
protected RegionSizeProperties<Point1S> computeRegionSizeProperties() {
if (isFull()) {
return new RegionSizeProperties<>(Geometry.TWO_PI, null);
}
double size = 0;
double scaledBarycenterSum = 0;
double intervalSize;
for (AngularInterval interval : toIntervals()) {
intervalSize = interval.getSize();
size += intervalSize;
scaledBarycenterSum += intervalSize * interval.getBarycenter().getNormalizedAzimuth();
}
final Point1S barycenter = size > 0 ?
Point1S.of(scaledBarycenterSum / size) :
null;
return new RegionSizeProperties<>(size, barycenter);
}
/** {@inheritDoc} */
@Override
protected RegionNode1S createNode() {
return new RegionNode1S(this);
}
/** Return a new, empty BSP tree.
* @return a new, empty BSP tree.
*/
public static RegionBSPTree1S empty() {
return new RegionBSPTree1S(false);
}
/** Return a new, full BSP tree. The returned tree represents the
* full space.
* @return a new, full BSP tree.
*/
public static RegionBSPTree1S full() {
return new RegionBSPTree1S(true);
}
/** Return a new BSP tree representing the same region as the given angular interval.
* @param interval the input interval
* @return a new BSP tree representing the same region as the given angular interval
*/
public static RegionBSPTree1S fromInterval(final AngularInterval interval) {
final CutAngle minBoundary = interval.getMinBoundary();
final CutAngle maxBoundary = interval.getMaxBoundary();
final RegionBSPTree1S tree = full();
if (minBoundary != null) {
tree.insert(minBoundary.span());
}
if (maxBoundary != null) {
tree.insert(maxBoundary.span());
}
return tree;
}
/** Perform a union operation with {@code target} and {@code input}, storing the result
* in {@code target}; does nothing if {@code input} is null.
* @param target target tree
* @param input input tree
*/
private static void safeUnion(final RegionBSPTree1S target, final RegionBSPTree1S input) {
if (input != null) {
target.union(input);
}
}
/** BSP tree node for one dimensional spherical space.
*/
public static final class RegionNode1S extends AbstractRegionBSPTree.AbstractRegionNode<Point1S, RegionNode1S> {
/** Simple constructor.
* @param tree the owning tree instance
*/
protected RegionNode1S(final AbstractBSPTree<Point1S, RegionNode1S> tree) {
super(tree);
}
/** {@inheritDoc} */
@Override
protected RegionNode1S getSelf() {
return this;
}
}
/** Internal class containing pairs of interval boundaries.
*/
private static final class BoundaryPair {
/** The min boundary. */
private final CutAngle min;
/** The max boundary. */
private final CutAngle max;
/** Simple constructor.
* @param min min boundary hyperplane
* @param max max boundary hyperplane
*/
BoundaryPair(final CutAngle min, final CutAngle max) {
this.min = min;
this.max = max;
}
/** Get the minimum boundary hyperplane.
* @return the minimum boundary hyperplane.
*/
public CutAngle getMin() {
return min;
}
/** Get the maximum boundary hyperplane.
* @return the maximum boundary hyperplane.
*/
public CutAngle getMax() {
return max;
}
/** Get the minumum value of the interval or zero if no minimum value exists.
* @return the minumum value of the interval or zero
* if no minimum value exists.
*/
public double getMinValue() {
return (min != null) ? min.getNormalizedAzimuth() : 0;
}
}
/** Class used to project points onto the region boundary.
*/
private static final class BoundaryProjector1S extends BoundaryProjector<Point1S, RegionNode1S> {
/** Simple constructor.
* @param point the point to project onto the region's boundary
*/
BoundaryProjector1S(final Point1S point) {
super(point);
}
/** {@inheritDoc} */
@Override
protected boolean isPossibleClosestCut(final SubHyperplane<Point1S> cut, final Point1S target,
final double minDist) {
// since the space wraps around, consider any cut as possibly being the closest
return true;
}
/** {@inheritDoc} */
@Override
protected Point1S disambiguateClosestPoint(final Point1S target, final Point1S a, final Point1S b) {
// prefer the point with the smaller normalize azimuth value
return a.getNormalizedAzimuth() < b.getNormalizedAzimuth() ? a : b;
}
}
}