View Javadoc

    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
    19  import java.util.Collections;
    20  import java.util.List;
    21  import java.util.Objects;
    22
    23  import org.apache.commons.geometry.core.RegionLocation;
    24  import org.apache.commons.geometry.core.Transform;
    25  import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
    26  import org.apache.commons.geometry.core.partitioning.Hyperplane;
    27  import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
    28  import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
    29  import org.apache.commons.geometry.core.partitioning.Split;
    30  import org.apache.commons.numbers.core.Precision;
    31
    32  /** Class representing an oriented point in 1-dimensional spherical space,
    33   * meaning an azimuth angle and a direction (increasing or decreasing angles)
    34   * along the circle.
    35   *
    36   * <p>Hyperplanes split the spaces they are embedded in into three distinct parts:
    37   * the hyperplane itself, a plus side and a minus side. However, since spherical
    38   * space wraps around, a single oriented point is not sufficient to partition the space;
    39   * any point could be classified as being on the plus or minus side of a hyperplane
    40   * depending on the direction that the circle is traversed. The approach taken in this
    41   * class to address this issue is to (1) define a second, implicit cut point at {@code 0pi} and
    42   * (2) define the domain of hyperplane points (for partitioning purposes) to be the
    43   * range {@code [0, 2pi)}. Each hyperplane then splits the space into the intervals
    44   * {@code [0, x]} and {@code [x, 2pi)}, where {@code x} is the location of the hyperplane.
    45   * One way to visualize this is to picture the circle as a cake that has already been
    46   * cut at {@code 0pi}. Each hyperplane then specifies the location of the second
    47   * cut of the cake, with the plus and minus sides being the pieces thus cut.
    48   * </p>
    49   *
    50   * <p>Note that with the hyperplane partitioning rules described above, the hyperplane
    51   * at {@code 0pi} is unique in that it has the entire space on one side (minus the hyperplane
    52   * itself) and no points whatsoever on the other. This is very different from hyperplanes in
    53   * Euclidean space, which always have infinitely many points on both sides.</p>
    54   *
    55   * <p>Instances of this class are guaranteed to be immutable.</p>
    56   * @see CutAngles
    57   */
    58  public final class CutAngle extends AbstractHyperplane<Point1S> {
    59      /** Hyperplane location as a point. */
    60      private final Point1S point;
    61
    62      /** Hyperplane direction. */
    63      private final boolean positiveFacing;
    64
    65      /** Simple constructor.
    66       * @param point location of the hyperplane
    67       * @param positiveFacing if true, the hyperplane will point in a positive angular
    68       *      direction; otherwise, it will point in a negative direction
    69       * @param precision precision context used to compare floating point values
    70       */
    71      CutAngle(final Point1S point, final boolean positiveFacing,
    72              final Precision.DoubleEquivalence precision) {
    73          super(precision);
    74
    75          this.point = point;
    76          this.positiveFacing = positiveFacing;
    77      }
    78
    79      /** Get the location of the hyperplane as a point.
    80       * @return the hyperplane location as a point
    81       * @see #getAzimuth()
    82       */
    83      public Point1S getPoint() {
    84          return point;
    85      }
    86
    87      /** Get the location of the hyperplane as a single value. This is
    88       * equivalent to {@code cutAngle.getPoint().getAzimuth()}.
    89       * @return the location of the hyperplane as a single value.
    90       * @see #getPoint()
    91       * @see Point1S#getAzimuth()
    92       */
    93      public double getAzimuth() {
    94          return point.getAzimuth();
    95      }
    96
    97      /** Get the location of the hyperplane as a single value, normalized
    98       * to the range {@code [0, 2pi)}. This is equivalent to
    99       * {@code cutAngle.getPoint().getNormalizedAzimuth()}.
    100      * @return the location of the hyperplane, normalized to the range
    101      *      {@code [0, 2pi)}
    102      * @see #getPoint()
    103      * @see Point1S#getNormalizedAzimuth()
    104      */
    105     public double getNormalizedAzimuth() {
    106         return point.getNormalizedAzimuth();
    107     }
    108
    109     /** Return true if the hyperplane is oriented with its plus
    110      * side pointing toward increasing angles.
    111      * @return true if the hyperplane is facing in the direction
    112      *      of increasing angles
    113      */
    114     public boolean isPositiveFacing() {
    115         return positiveFacing;
    116     }
    117
    118     /** Return true if this instance should be considered equivalent to the argument, using the
    119      * given precision context for comparison.
    120      * <p>The instances are considered equivalent if they
    121      * <ol>
    122      *    <li>have equivalent point locations (points separated by multiples of 2pi are
    123      *      considered equivalent) and
    124      *    <li>point in the same direction.</li>
    125      * </ol>
    126      * @param other point to compare with
    127      * @param precision precision context to use for the comparison
    128      * @return true if this instance should be considered equivalent to the argument
    129      * @see Point1S#eq(Point1S, Precision.DoubleEquivalence)
    130      */
    131     public boolean eq(final CutAngle other, final Precision.DoubleEquivalence precision) {
    132         return point.eq(other.point, precision) &&
    133                 positiveFacing == other.positiveFacing;
    134     }
    135
    136     /** {@inheritDoc} */
    137     @Override
    138     public double offset(final Point1S pt) {
    139         final double dist = pt.getNormalizedAzimuth() - this.point.getNormalizedAzimuth();
    140         return positiveFacing ? +dist : -dist;
    141     }
    142
    143     /** {@inheritDoc} */
    144     @Override
    145     public HyperplaneLocation classify(final Point1S pt) {
    146         final Precision.DoubleEquivalence precision = getPrecision();
    147
    148         final Point1S compPt = Point1S.ZERO.eq(pt, precision) ?
    149                 Point1S.ZERO :
    150                 pt;
    151
    152         final double offsetValue = offset(compPt);
    153         final double cmp = precision.signum(offsetValue);
    154
    155         if (cmp > 0) {
    156             return HyperplaneLocation.PLUS;
    157         } else if (cmp < 0) {
    158             return HyperplaneLocation.MINUS;
    159         }
    160
    161         return HyperplaneLocation.ON;
    162     }
    163
    164     /** {@inheritDoc} */
    165     @Override
    166     public Point1S project(final Point1S pt) {
    167         return this.point;
    168     }
    169
    170     /** {@inheritDoc} */
    171     @Override
    172     public CutAngle reverse() {
    173         return new CutAngle(point, !positiveFacing, getPrecision());
    174     }
    175
    176     /** {@inheritDoc} */
    177     @Override
    178     public CutAngle transform(final Transform<Point1S> transform) {
    179         final Point1S tPoint = transform.apply(point);
    180         final boolean tPositiveFacing = transform.preservesOrientation() == positiveFacing;
    181
    182         return CutAngles.fromPointAndDirection(tPoint, tPositiveFacing, getPrecision());
    183     }
    184
    185     /** {@inheritDoc} */
    186     @Override
    187     public boolean similarOrientation(final Hyperplane<Point1S> other) {
    188         return positiveFacing == ((CutAngle) other).positiveFacing;
    189     }
    190
    191     /** {@inheritDoc}
    192      *
    193      * <p>Since there are no subspaces in spherical 1D space, this method effectively returns a stub implementation
    194      * of {@link HyperplaneConvexSubset}, the main purpose of which is to support the proper functioning
    195      * of the partitioning code.</p>
    196      */
    197     @Override
    198     public HyperplaneConvexSubset<Point1S> span() {
    199         return new CutAngleConvexSubset(this);
    200     }
    201
    202     /** {@inheritDoc} */
    203     @Override
    204     public int hashCode() {
    205         return Objects.hash(point, positiveFacing, getPrecision());
    206     }
    207
    208     /** {@inheritDoc} */
    209     @Override
    210     public boolean equals(final Object obj) {
    211         if (this == obj) {
    212             return true;
    213         } else if (!(obj instanceof CutAngle)) {
    214             return false;
    215         }
    216
    217         final CutAngle other = (CutAngle) obj;
    218         return Objects.equals(getPrecision(), other.getPrecision()) &&
    219                 Objects.equals(point, other.point) &&
    220                 positiveFacing == other.positiveFacing;
    221     }
    222
    223     /** {@inheritDoc} */
    224     @Override
    225     public String toString() {
    226         final StringBuilder sb = new StringBuilder();
    227         sb.append(this.getClass().getSimpleName())
    228             .append("[point= ")
    229             .append(point)
    230             .append(", positiveFacing= ")
    231             .append(isPositiveFacing())
    232             .append(']');
    233
    234         return sb.toString();
    235     }
    236
    237     /** {@link HyperplaneConvexSubset} implementation for spherical 1D space. Since there are no subspaces in 1D,
    238      * this is effectively a stub implementation, its main use being to allow for the correct functioning of
    239      * partitioning code.
    240      */
    241     private static final class CutAngleConvexSubset implements HyperplaneConvexSubset<Point1S> {
    242         /** The hyperplane containing for this instance. */
    243         private final CutAngle hyperplane;
    244
    245         /** Simple constructor.
    246          * @param hyperplane containing hyperplane instance
    247          */
    248         CutAngleConvexSubset(final CutAngle hyperplane) {
    249             this.hyperplane = hyperplane;
    250         }
    251
    252         /** {@inheritDoc} */
    253         @Override
    254         public CutAngle getHyperplane() {
    255             return hyperplane;
    256         }
    257
    258         /** {@inheritDoc}
    259         *
    260         * <p>This method always returns {@code false}.</p>
    261         */
    262         @Override
    263         public boolean isFull() {
    264             return false;
    265         }
    266
    267         /** {@inheritDoc}
    268         *
    269         * <p>This method always returns {@code false}.</p>
    270         */
    271         @Override
    272         public boolean isEmpty() {
    273             return false;
    274         }
    275
    276         /** {@inheritDoc}
    277          *
    278          * <p>This method always returns {@code false}.</p>
    279          */
    280         @Override
    281         public boolean isInfinite() {
    282             return false;
    283         }
    284
    285         /** {@inheritDoc}
    286         *
    287         * <p>This method always returns {@code true}.</p>
    288         */
    289         @Override
    290         public boolean isFinite() {
    291             return true;
    292         }
    293
    294         /** {@inheritDoc}
    295          *
    296          *  <p>This method always returns {@code 0}.</p>
    297          */
    298         @Override
    299         public double getSize() {
    300             return 0;
    301         }
    302
    303         /** {@inheritDoc}
    304          *
    305          * <p>This method returns the point for the underlying hyperplane.</p>
    306          */
    307         @Override
    308         public Point1S getCentroid() {
    309             return hyperplane.getPoint();
    310         }
    311
    312         /** {@inheritDoc}
    313          *
    314          * <p>This method returns {@link RegionLocation#BOUNDARY} if the
    315          * point is on the hyperplane and {@link RegionLocation#OUTSIDE}
    316          * otherwise.</p>
    317          */
    318         @Override
    319         public RegionLocation classify(final Point1S point) {
    320             if (hyperplane.contains(point)) {
    321                 return RegionLocation.BOUNDARY;
    322             }
    323
    324             return RegionLocation.OUTSIDE;
    325         }
    326
    327         /** {@inheritDoc} */
    328         @Override
    329         public Point1S closest(final Point1S point) {
    330             return hyperplane.project(point);
    331         }
    332
    333         /** {@inheritDoc} */
    334         @Override
    335         public Split<CutAngleConvexSubset> split(final Hyperplane<Point1S> splitter) {
    336             final HyperplaneLocation side = splitter.classify(hyperplane.getPoint());
    337
    338             CutAngleConvexSubset minus = null;
    339             CutAngleConvexSubset plus = null;
    340
    341             if (side == HyperplaneLocation.MINUS) {
    342                 minus = this;
    343             } else if (side == HyperplaneLocation.PLUS) {
    344                 plus = this;
    345             }
    346
    347             return new Split<>(minus, plus);
    348         }
    349
    350         /** {@inheritDoc} */
    351         @Override
    352         public List<CutAngleConvexSubset> toConvex() {
    353             return Collections.singletonList(this);
    354         }
    355
    356         /** {@inheritDoc} */
    357         @Override
    358         public CutAngleConvexSubset transform(final Transform<Point1S> transform) {
    359             return new CutAngleConvexSubset(getHyperplane().transform(transform));
    360         }
    361
    362         /** {@inheritDoc} */
    363         @Override
    364         public CutAngleConvexSubset reverse() {
    365             return new CutAngleConvexSubset(hyperplane.reverse());
    366         }
    367
    368         /** {@inheritDoc} */
    369         @Override
    370         public String toString() {
    371             final StringBuilder sb = new StringBuilder();
    372             sb.append(this.getClass().getSimpleName())
    373                 .append("[hyperplane= ")
    374                 .append(hyperplane)
    375                 .append(']');
    376
    377             return sb.toString();
    378         }
    379     }
    380 }