CutAngle.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.geometry.spherical.oned;

  18. import java.util.Collections;
  19. import java.util.List;
  20. import java.util.Objects;

  21. import org.apache.commons.geometry.core.RegionLocation;
  22. import org.apache.commons.geometry.core.Transform;
  23. import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
  24. import org.apache.commons.geometry.core.partitioning.Hyperplane;
  25. import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
  26. import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
  27. import org.apache.commons.geometry.core.partitioning.Split;
  28. import org.apache.commons.numbers.core.Precision;

  29. /** Class representing an oriented point in 1-dimensional spherical space,
  30.  * meaning an azimuth angle and a direction (increasing or decreasing angles)
  31.  * along the circle.
  32.  *
  33.  * <p>Hyperplanes split the spaces they are embedded in into three distinct parts:
  34.  * the hyperplane itself, a plus side and a minus side. However, since spherical
  35.  * space wraps around, a single oriented point is not sufficient to partition the space;
  36.  * any point could be classified as being on the plus or minus side of a hyperplane
  37.  * depending on the direction that the circle is traversed. The approach taken in this
  38.  * class to address this issue is to (1) define a second, implicit cut point at {@code 0pi} and
  39.  * (2) define the domain of hyperplane points (for partitioning purposes) to be the
  40.  * range {@code [0, 2pi)}. Each hyperplane then splits the space into the intervals
  41.  * {@code [0, x]} and {@code [x, 2pi)}, where {@code x} is the location of the hyperplane.
  42.  * One way to visualize this is to picture the circle as a cake that has already been
  43.  * cut at {@code 0pi}. Each hyperplane then specifies the location of the second
  44.  * cut of the cake, with the plus and minus sides being the pieces thus cut.
  45.  * </p>
  46.  *
  47.  * <p>Note that with the hyperplane partitioning rules described above, the hyperplane
  48.  * at {@code 0pi} is unique in that it has the entire space on one side (minus the hyperplane
  49.  * itself) and no points whatsoever on the other. This is very different from hyperplanes in
  50.  * Euclidean space, which always have infinitely many points on both sides.</p>
  51.  *
  52.  * <p>Instances of this class are guaranteed to be immutable.</p>
  53.  * @see CutAngles
  54.  */
  55. public final class CutAngle extends AbstractHyperplane<Point1S> {
  56.     /** Hyperplane location as a point. */
  57.     private final Point1S point;

  58.     /** Hyperplane direction. */
  59.     private final boolean positiveFacing;

  60.     /** Simple constructor.
  61.      * @param point location of the hyperplane
  62.      * @param positiveFacing if true, the hyperplane will point in a positive angular
  63.      *      direction; otherwise, it will point in a negative direction
  64.      * @param precision precision context used to compare floating point values
  65.      */
  66.     CutAngle(final Point1S point, final boolean positiveFacing,
  67.             final Precision.DoubleEquivalence precision) {
  68.         super(precision);

  69.         this.point = point;
  70.         this.positiveFacing = positiveFacing;
  71.     }

  72.     /** Get the location of the hyperplane as a point.
  73.      * @return the hyperplane location as a point
  74.      * @see #getAzimuth()
  75.      */
  76.     public Point1S getPoint() {
  77.         return point;
  78.     }

  79.     /** Get the location of the hyperplane as a single value. This is
  80.      * equivalent to {@code cutAngle.getPoint().getAzimuth()}.
  81.      * @return the location of the hyperplane as a single value.
  82.      * @see #getPoint()
  83.      * @see Point1S#getAzimuth()
  84.      */
  85.     public double getAzimuth() {
  86.         return point.getAzimuth();
  87.     }

  88.     /** Get the location of the hyperplane as a single value, normalized
  89.      * to the range {@code [0, 2pi)}. This is equivalent to
  90.      * {@code cutAngle.getPoint().getNormalizedAzimuth()}.
  91.      * @return the location of the hyperplane, normalized to the range
  92.      *      {@code [0, 2pi)}
  93.      * @see #getPoint()
  94.      * @see Point1S#getNormalizedAzimuth()
  95.      */
  96.     public double getNormalizedAzimuth() {
  97.         return point.getNormalizedAzimuth();
  98.     }

  99.     /** Return true if the hyperplane is oriented with its plus
  100.      * side pointing toward increasing angles.
  101.      * @return true if the hyperplane is facing in the direction
  102.      *      of increasing angles
  103.      */
  104.     public boolean isPositiveFacing() {
  105.         return positiveFacing;
  106.     }

  107.     /** Return true if this instance should be considered equivalent to the argument, using the
  108.      * given precision context for comparison.
  109.      * <p>The instances are considered equivalent if they
  110.      * <ol>
  111.      *    <li>have equivalent point locations (points separated by multiples of 2pi are
  112.      *      considered equivalent) and
  113.      *    <li>point in the same direction.</li>
  114.      * </ol>
  115.      * @param other point to compare with
  116.      * @param precision precision context to use for the comparison
  117.      * @return true if this instance should be considered equivalent to the argument
  118.      * @see Point1S#eq(Point1S, Precision.DoubleEquivalence)
  119.      */
  120.     public boolean eq(final CutAngle other, final Precision.DoubleEquivalence precision) {
  121.         return point.eq(other.point, precision) &&
  122.                 positiveFacing == other.positiveFacing;
  123.     }

  124.     /** {@inheritDoc} */
  125.     @Override
  126.     public double offset(final Point1S pt) {
  127.         final double dist = pt.getNormalizedAzimuth() - this.point.getNormalizedAzimuth();
  128.         return positiveFacing ? +dist : -dist;
  129.     }

  130.     /** {@inheritDoc} */
  131.     @Override
  132.     public HyperplaneLocation classify(final Point1S pt) {
  133.         final Precision.DoubleEquivalence precision = getPrecision();

  134.         final Point1S compPt = Point1S.ZERO.eq(pt, precision) ?
  135.                 Point1S.ZERO :
  136.                 pt;

  137.         final double offsetValue = offset(compPt);
  138.         final double cmp = precision.signum(offsetValue);

  139.         if (cmp > 0) {
  140.             return HyperplaneLocation.PLUS;
  141.         } else if (cmp < 0) {
  142.             return HyperplaneLocation.MINUS;
  143.         }

  144.         return HyperplaneLocation.ON;
  145.     }

  146.     /** {@inheritDoc} */
  147.     @Override
  148.     public Point1S project(final Point1S pt) {
  149.         return this.point;
  150.     }

  151.     /** {@inheritDoc} */
  152.     @Override
  153.     public CutAngle reverse() {
  154.         return new CutAngle(point, !positiveFacing, getPrecision());
  155.     }

  156.     /** {@inheritDoc} */
  157.     @Override
  158.     public CutAngle transform(final Transform<Point1S> transform) {
  159.         final Point1S tPoint = transform.apply(point);
  160.         final boolean tPositiveFacing = transform.preservesOrientation() == positiveFacing;

  161.         return CutAngles.fromPointAndDirection(tPoint, tPositiveFacing, getPrecision());
  162.     }

  163.     /** {@inheritDoc} */
  164.     @Override
  165.     public boolean similarOrientation(final Hyperplane<Point1S> other) {
  166.         return positiveFacing == ((CutAngle) other).positiveFacing;
  167.     }

  168.     /** {@inheritDoc}
  169.      *
  170.      * <p>Since there are no subspaces in spherical 1D space, this method effectively returns a stub implementation
  171.      * of {@link HyperplaneConvexSubset}, the main purpose of which is to support the proper functioning
  172.      * of the partitioning code.</p>
  173.      */
  174.     @Override
  175.     public HyperplaneConvexSubset<Point1S> span() {
  176.         return new CutAngleConvexSubset(this);
  177.     }

  178.     /** {@inheritDoc} */
  179.     @Override
  180.     public int hashCode() {
  181.         return Objects.hash(point, positiveFacing, getPrecision());
  182.     }

  183.     /** {@inheritDoc} */
  184.     @Override
  185.     public boolean equals(final Object obj) {
  186.         if (this == obj) {
  187.             return true;
  188.         } else if (!(obj instanceof CutAngle)) {
  189.             return false;
  190.         }

  191.         final CutAngle other = (CutAngle) obj;
  192.         return Objects.equals(getPrecision(), other.getPrecision()) &&
  193.                 Objects.equals(point, other.point) &&
  194.                 positiveFacing == other.positiveFacing;
  195.     }

  196.     /** {@inheritDoc} */
  197.     @Override
  198.     public String toString() {
  199.         final StringBuilder sb = new StringBuilder();
  200.         sb.append(this.getClass().getSimpleName())
  201.             .append("[point= ")
  202.             .append(point)
  203.             .append(", positiveFacing= ")
  204.             .append(isPositiveFacing())
  205.             .append(']');

  206.         return sb.toString();
  207.     }

  208.     /** {@link HyperplaneConvexSubset} implementation for spherical 1D space. Since there are no subspaces in 1D,
  209.      * this is effectively a stub implementation, its main use being to allow for the correct functioning of
  210.      * partitioning code.
  211.      */
  212.     private static final class CutAngleConvexSubset implements HyperplaneConvexSubset<Point1S> {
  213.         /** The hyperplane containing for this instance. */
  214.         private final CutAngle hyperplane;

  215.         /** Simple constructor.
  216.          * @param hyperplane containing hyperplane instance
  217.          */
  218.         CutAngleConvexSubset(final CutAngle hyperplane) {
  219.             this.hyperplane = hyperplane;
  220.         }

  221.         /** {@inheritDoc} */
  222.         @Override
  223.         public CutAngle getHyperplane() {
  224.             return hyperplane;
  225.         }

  226.         /** {@inheritDoc}
  227.         *
  228.         * <p>This method always returns {@code false}.</p>
  229.         */
  230.         @Override
  231.         public boolean isFull() {
  232.             return false;
  233.         }

  234.         /** {@inheritDoc}
  235.         *
  236.         * <p>This method always returns {@code false}.</p>
  237.         */
  238.         @Override
  239.         public boolean isEmpty() {
  240.             return false;
  241.         }

  242.         /** {@inheritDoc}
  243.          *
  244.          * <p>This method always returns {@code false}.</p>
  245.          */
  246.         @Override
  247.         public boolean isInfinite() {
  248.             return false;
  249.         }

  250.         /** {@inheritDoc}
  251.         *
  252.         * <p>This method always returns {@code true}.</p>
  253.         */
  254.         @Override
  255.         public boolean isFinite() {
  256.             return true;
  257.         }

  258.         /** {@inheritDoc}
  259.          *
  260.          *  <p>This method always returns {@code 0}.</p>
  261.          */
  262.         @Override
  263.         public double getSize() {
  264.             return 0;
  265.         }

  266.         /** {@inheritDoc}
  267.          *
  268.          * <p>This method returns the point for the underlying hyperplane.</p>
  269.          */
  270.         @Override
  271.         public Point1S getCentroid() {
  272.             return hyperplane.getPoint();
  273.         }

  274.         /** {@inheritDoc}
  275.          *
  276.          * <p>This method returns {@link RegionLocation#BOUNDARY} if the
  277.          * point is on the hyperplane and {@link RegionLocation#OUTSIDE}
  278.          * otherwise.</p>
  279.          */
  280.         @Override
  281.         public RegionLocation classify(final Point1S point) {
  282.             if (hyperplane.contains(point)) {
  283.                 return RegionLocation.BOUNDARY;
  284.             }

  285.             return RegionLocation.OUTSIDE;
  286.         }

  287.         /** {@inheritDoc} */
  288.         @Override
  289.         public Point1S closest(final Point1S point) {
  290.             return hyperplane.project(point);
  291.         }

  292.         /** {@inheritDoc} */
  293.         @Override
  294.         public Split<CutAngleConvexSubset> split(final Hyperplane<Point1S> splitter) {
  295.             final HyperplaneLocation side = splitter.classify(hyperplane.getPoint());

  296.             CutAngleConvexSubset minus = null;
  297.             CutAngleConvexSubset plus = null;

  298.             if (side == HyperplaneLocation.MINUS) {
  299.                 minus = this;
  300.             } else if (side == HyperplaneLocation.PLUS) {
  301.                 plus = this;
  302.             }

  303.             return new Split<>(minus, plus);
  304.         }

  305.         /** {@inheritDoc} */
  306.         @Override
  307.         public List<CutAngleConvexSubset> toConvex() {
  308.             return Collections.singletonList(this);
  309.         }

  310.         /** {@inheritDoc} */
  311.         @Override
  312.         public CutAngleConvexSubset transform(final Transform<Point1S> transform) {
  313.             return new CutAngleConvexSubset(getHyperplane().transform(transform));
  314.         }

  315.         /** {@inheritDoc} */
  316.         @Override
  317.         public CutAngleConvexSubset reverse() {
  318.             return new CutAngleConvexSubset(hyperplane.reverse());
  319.         }

  320.         /** {@inheritDoc} */
  321.         @Override
  322.         public String toString() {
  323.             final StringBuilder sb = new StringBuilder();
  324.             sb.append(this.getClass().getSimpleName())
  325.                 .append("[hyperplane= ")
  326.                 .append(hyperplane)
  327.                 .append(']');

  328.             return sb.toString();
  329.         }
  330.     }
  331. }