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 }