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 }