CutAngle.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
- *
- * 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.Collections;
- import java.util.List;
- import java.util.Objects;
- import org.apache.commons.geometry.core.RegionLocation;
- import org.apache.commons.geometry.core.Transform;
- import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
- import org.apache.commons.geometry.core.partitioning.Hyperplane;
- import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
- import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
- import org.apache.commons.geometry.core.partitioning.Split;
- import org.apache.commons.numbers.core.Precision;
- /** Class representing an oriented point in 1-dimensional spherical space,
- * meaning an azimuth angle and a direction (increasing or decreasing angles)
- * along the circle.
- *
- * <p>Hyperplanes split the spaces they are embedded in into three distinct parts:
- * the hyperplane itself, a plus side and a minus side. However, since spherical
- * space wraps around, a single oriented point is not sufficient to partition the space;
- * any point could be classified as being on the plus or minus side of a hyperplane
- * depending on the direction that the circle is traversed. The approach taken in this
- * class to address this issue is to (1) define a second, implicit cut point at {@code 0pi} and
- * (2) define the domain of hyperplane points (for partitioning purposes) to be the
- * range {@code [0, 2pi)}. Each hyperplane then splits the space into the intervals
- * {@code [0, x]} and {@code [x, 2pi)}, where {@code x} is the location of the hyperplane.
- * One way to visualize this is to picture the circle as a cake that has already been
- * cut at {@code 0pi}. Each hyperplane then specifies the location of the second
- * cut of the cake, with the plus and minus sides being the pieces thus cut.
- * </p>
- *
- * <p>Note that with the hyperplane partitioning rules described above, the hyperplane
- * at {@code 0pi} is unique in that it has the entire space on one side (minus the hyperplane
- * itself) and no points whatsoever on the other. This is very different from hyperplanes in
- * Euclidean space, which always have infinitely many points on both sides.</p>
- *
- * <p>Instances of this class are guaranteed to be immutable.</p>
- * @see CutAngles
- */
- public final class CutAngle extends AbstractHyperplane<Point1S> {
- /** Hyperplane location as a point. */
- private final Point1S point;
- /** Hyperplane direction. */
- private final boolean positiveFacing;
- /** Simple constructor.
- * @param point location of the hyperplane
- * @param positiveFacing if true, the hyperplane will point in a positive angular
- * direction; otherwise, it will point in a negative direction
- * @param precision precision context used to compare floating point values
- */
- CutAngle(final Point1S point, final boolean positiveFacing,
- final Precision.DoubleEquivalence precision) {
- super(precision);
- this.point = point;
- this.positiveFacing = positiveFacing;
- }
- /** Get the location of the hyperplane as a point.
- * @return the hyperplane location as a point
- * @see #getAzimuth()
- */
- public Point1S getPoint() {
- return point;
- }
- /** Get the location of the hyperplane as a single value. This is
- * equivalent to {@code cutAngle.getPoint().getAzimuth()}.
- * @return the location of the hyperplane as a single value.
- * @see #getPoint()
- * @see Point1S#getAzimuth()
- */
- public double getAzimuth() {
- return point.getAzimuth();
- }
- /** Get the location of the hyperplane as a single value, normalized
- * to the range {@code [0, 2pi)}. This is equivalent to
- * {@code cutAngle.getPoint().getNormalizedAzimuth()}.
- * @return the location of the hyperplane, normalized to the range
- * {@code [0, 2pi)}
- * @see #getPoint()
- * @see Point1S#getNormalizedAzimuth()
- */
- public double getNormalizedAzimuth() {
- return point.getNormalizedAzimuth();
- }
- /** Return true if the hyperplane is oriented with its plus
- * side pointing toward increasing angles.
- * @return true if the hyperplane is facing in the direction
- * of increasing angles
- */
- public boolean isPositiveFacing() {
- return positiveFacing;
- }
- /** Return true if this instance should be considered equivalent to the argument, using the
- * given precision context for comparison.
- * <p>The instances are considered equivalent if they
- * <ol>
- * <li>have equivalent point locations (points separated by multiples of 2pi are
- * considered equivalent) and
- * <li>point in the same direction.</li>
- * </ol>
- * @param other point to compare with
- * @param precision precision context to use for the comparison
- * @return true if this instance should be considered equivalent to the argument
- * @see Point1S#eq(Point1S, Precision.DoubleEquivalence)
- */
- public boolean eq(final CutAngle other, final Precision.DoubleEquivalence precision) {
- return point.eq(other.point, precision) &&
- positiveFacing == other.positiveFacing;
- }
- /** {@inheritDoc} */
- @Override
- public double offset(final Point1S pt) {
- final double dist = pt.getNormalizedAzimuth() - this.point.getNormalizedAzimuth();
- return positiveFacing ? +dist : -dist;
- }
- /** {@inheritDoc} */
- @Override
- public HyperplaneLocation classify(final Point1S pt) {
- final Precision.DoubleEquivalence precision = getPrecision();
- final Point1S compPt = Point1S.ZERO.eq(pt, precision) ?
- Point1S.ZERO :
- pt;
- final double offsetValue = offset(compPt);
- final double cmp = precision.signum(offsetValue);
- if (cmp > 0) {
- return HyperplaneLocation.PLUS;
- } else if (cmp < 0) {
- return HyperplaneLocation.MINUS;
- }
- return HyperplaneLocation.ON;
- }
- /** {@inheritDoc} */
- @Override
- public Point1S project(final Point1S pt) {
- return this.point;
- }
- /** {@inheritDoc} */
- @Override
- public CutAngle reverse() {
- return new CutAngle(point, !positiveFacing, getPrecision());
- }
- /** {@inheritDoc} */
- @Override
- public CutAngle transform(final Transform<Point1S> transform) {
- final Point1S tPoint = transform.apply(point);
- final boolean tPositiveFacing = transform.preservesOrientation() == positiveFacing;
- return CutAngles.fromPointAndDirection(tPoint, tPositiveFacing, getPrecision());
- }
- /** {@inheritDoc} */
- @Override
- public boolean similarOrientation(final Hyperplane<Point1S> other) {
- return positiveFacing == ((CutAngle) other).positiveFacing;
- }
- /** {@inheritDoc}
- *
- * <p>Since there are no subspaces in spherical 1D space, this method effectively returns a stub implementation
- * of {@link HyperplaneConvexSubset}, the main purpose of which is to support the proper functioning
- * of the partitioning code.</p>
- */
- @Override
- public HyperplaneConvexSubset<Point1S> span() {
- return new CutAngleConvexSubset(this);
- }
- /** {@inheritDoc} */
- @Override
- public int hashCode() {
- return Objects.hash(point, positiveFacing, getPrecision());
- }
- /** {@inheritDoc} */
- @Override
- public boolean equals(final Object obj) {
- if (this == obj) {
- return true;
- } else if (!(obj instanceof CutAngle)) {
- return false;
- }
- final CutAngle other = (CutAngle) obj;
- return Objects.equals(getPrecision(), other.getPrecision()) &&
- Objects.equals(point, other.point) &&
- positiveFacing == other.positiveFacing;
- }
- /** {@inheritDoc} */
- @Override
- public String toString() {
- final StringBuilder sb = new StringBuilder();
- sb.append(this.getClass().getSimpleName())
- .append("[point= ")
- .append(point)
- .append(", positiveFacing= ")
- .append(isPositiveFacing())
- .append(']');
- return sb.toString();
- }
- /** {@link HyperplaneConvexSubset} implementation for spherical 1D space. Since there are no subspaces in 1D,
- * this is effectively a stub implementation, its main use being to allow for the correct functioning of
- * partitioning code.
- */
- private static final class CutAngleConvexSubset implements HyperplaneConvexSubset<Point1S> {
- /** The hyperplane containing for this instance. */
- private final CutAngle hyperplane;
- /** Simple constructor.
- * @param hyperplane containing hyperplane instance
- */
- CutAngleConvexSubset(final CutAngle hyperplane) {
- this.hyperplane = hyperplane;
- }
- /** {@inheritDoc} */
- @Override
- public CutAngle getHyperplane() {
- return hyperplane;
- }
- /** {@inheritDoc}
- *
- * <p>This method always returns {@code false}.</p>
- */
- @Override
- public boolean isFull() {
- return false;
- }
- /** {@inheritDoc}
- *
- * <p>This method always returns {@code false}.</p>
- */
- @Override
- public boolean isEmpty() {
- return false;
- }
- /** {@inheritDoc}
- *
- * <p>This method always returns {@code false}.</p>
- */
- @Override
- public boolean isInfinite() {
- return false;
- }
- /** {@inheritDoc}
- *
- * <p>This method always returns {@code true}.</p>
- */
- @Override
- public boolean isFinite() {
- return true;
- }
- /** {@inheritDoc}
- *
- * <p>This method always returns {@code 0}.</p>
- */
- @Override
- public double getSize() {
- return 0;
- }
- /** {@inheritDoc}
- *
- * <p>This method returns the point for the underlying hyperplane.</p>
- */
- @Override
- public Point1S getCentroid() {
- return hyperplane.getPoint();
- }
- /** {@inheritDoc}
- *
- * <p>This method returns {@link RegionLocation#BOUNDARY} if the
- * point is on the hyperplane and {@link RegionLocation#OUTSIDE}
- * otherwise.</p>
- */
- @Override
- public RegionLocation classify(final Point1S point) {
- if (hyperplane.contains(point)) {
- return RegionLocation.BOUNDARY;
- }
- return RegionLocation.OUTSIDE;
- }
- /** {@inheritDoc} */
- @Override
- public Point1S closest(final Point1S point) {
- return hyperplane.project(point);
- }
- /** {@inheritDoc} */
- @Override
- public Split<CutAngleConvexSubset> split(final Hyperplane<Point1S> splitter) {
- final HyperplaneLocation side = splitter.classify(hyperplane.getPoint());
- CutAngleConvexSubset minus = null;
- CutAngleConvexSubset plus = null;
- if (side == HyperplaneLocation.MINUS) {
- minus = this;
- } else if (side == HyperplaneLocation.PLUS) {
- plus = this;
- }
- return new Split<>(minus, plus);
- }
- /** {@inheritDoc} */
- @Override
- public List<CutAngleConvexSubset> toConvex() {
- return Collections.singletonList(this);
- }
- /** {@inheritDoc} */
- @Override
- public CutAngleConvexSubset transform(final Transform<Point1S> transform) {
- return new CutAngleConvexSubset(getHyperplane().transform(transform));
- }
- /** {@inheritDoc} */
- @Override
- public CutAngleConvexSubset reverse() {
- return new CutAngleConvexSubset(hyperplane.reverse());
- }
- /** {@inheritDoc} */
- @Override
- public String toString() {
- final StringBuilder sb = new StringBuilder();
- sb.append(this.getClass().getSimpleName())
- .append("[hyperplane= ")
- .append(hyperplane)
- .append(']');
- return sb.toString();
- }
- }
- }