All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.commons.geometry.spherical.oned.RegionBSPTree1S Maven / Gradle / Ivy

The newest version!
/*
 * 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.Transform;
import org.apache.commons.geometry.core.partitioning.Hyperplane;
import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
import org.apache.commons.geometry.core.partitioning.HyperplaneSubset;
import org.apache.commons.geometry.core.partitioning.Split;
import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree;
import org.apache.commons.geometry.euclidean.twod.Vector2D;
import org.apache.commons.numbers.angle.Angle;
import org.apache.commons.numbers.core.Precision;

/** BSP tree representing regions in 1D spherical space.
 */
public class RegionBSPTree1S extends AbstractRegionBSPTree {
    /** Comparator used to sort BoundaryPairs by ascending azimuth.  */
    private static final Comparator BOUNDARY_PAIR_COMPARATOR =
            Comparator.comparingDouble(BoundaryPair::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(final 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() {
        final 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}
     *
     * 

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.

*/ @Override public void transform(final Transform transform) { if (!isFull() && !isEmpty()) { // transform each interval individually to handle wrap-around final List intervals = toIntervals(); setEmpty(); for (final AngularInterval interval : intervals) { union(interval.transform(transform).toTree()); } } } /** {@inheritDoc} * *

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.

* * @see CutAngle * @see #splitDiameter(CutAngle) */ @Override public Split split(final Hyperplane 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) { final 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 splitDiameter(final CutAngle splitter) { final CutAngle opposite = CutAngles.fromPointAndDirection( splitter.getPoint().antipodal(), !splitter.isPositiveFacing(), splitter.getPrecision()); final double plusPoleOffset = splitter.isPositiveFacing() ? +Angle.PI_OVER_TWO : -Angle.PI_OVER_TWO; final Point1S plusPole = Point1S.of(splitter.getAzimuth() + plusPoleOffset); final boolean zeroOnPlusSide = splitter.getPrecision() .lte(plusPole.distance(Point1S.ZERO), Angle.PI_OVER_TWO); final Split firstSplit = split(splitter); final Split 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 toIntervals() { if (isFull()) { return Collections.singletonList(AngularInterval.full()); } final List insideBoundaryPairs = new ArrayList<>(); for (final RegionNode1S node : nodes()) { if (node.isInside()) { insideBoundaryPairs.add(getNodeBoundaryPair(node)); } } insideBoundaryPairs.sort(BOUNDARY_PAIR_COMPARATOR); final int boundaryPairCount = insideBoundaryPairs.size(); // Get the start point for merging intervals together. final int startOffset = getIntervalStartIndex(insideBoundaryPairs); // Go through the pairs starting at the start offset and create intervals // for each set of adjacent pairs. final List intervals = new ArrayList<>(); BoundaryPair start = null; BoundaryPair end = null; BoundaryPair current; 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; } /** Get the index that should be used as the starting point for combining adjacent boundary pairs * into contiguous intervals. This is computed as the first boundary pair found that is not connected * to the pair before it, or {@code 0} if no such entry exists. * @param boundaryPairs list of boundary pairs to search; must be ordered by increasing azimuth * @return the index to use as a starting place for combining adjacent boundary pairs * into contiguous intervals */ private int getIntervalStartIndex(final List boundaryPairs) { final int size = boundaryPairs.size(); if (size > 0) { BoundaryPair current; BoundaryPair previous = boundaryPairs.get(size - 1); for (int i = 0; i < size; ++i, previous = current) { current = boundaryPairs.get(i); if (!Objects.equals(current.getMin(), previous.getMax())) { return i; } } } return 0; } /** 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(); final Precision.DoubleEquivalence 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 = CutAngles.createNegativeFacing(0.0, precision); } if (max != null) { if (!max.isPositiveFacing()) { max = max.reverse(); } } else { max = CutAngles.createPositiveFacing(Angle.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 computeRegionSizeProperties() { if (isFull()) { return new RegionSizeProperties<>(Angle.TWO_PI, null); } else if (isEmpty()) { return new RegionSizeProperties<>(0, null); } double size = 0; Vector2D scaledCentroidSum = Vector2D.ZERO; double intervalSize; for (final AngularInterval interval : toIntervals()) { intervalSize = interval.getSize(); size += intervalSize; scaledCentroidSum = scaledCentroidSum.add(interval.getCentroid().getVector().withNorm(intervalSize)); } final Precision.DoubleEquivalence precision = ((CutAngle) getRoot().getCutHyperplane()).getPrecision(); final Point1S centroid = scaledCentroidSum.eq(Vector2D.ZERO, precision) ? null : Point1S.from(scaledCentroidSum); return new RegionSizeProperties<>(size, centroid); } /** {@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 { /** Simple constructor. * @param tree the owning tree instance */ private RegionNode1S(final AbstractBSPTree 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 minimum value of the interval or zero if no minimum value exists. * @return the minimum 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 { /** 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 HyperplaneSubset 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; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy