001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.geometry.spherical.oned;
018
019import java.util.Collections;
020import java.util.List;
021import java.util.Objects;
022
023import org.apache.commons.geometry.core.RegionLocation;
024import org.apache.commons.geometry.core.Transform;
025import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
026import org.apache.commons.geometry.core.partitioning.Hyperplane;
027import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
028import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
029import org.apache.commons.geometry.core.partitioning.Split;
030import org.apache.commons.numbers.core.Precision;
031
032/** Class representing an oriented point in 1-dimensional spherical space,
033 * meaning an azimuth angle and a direction (increasing or decreasing angles)
034 * along the circle.
035 *
036 * <p>Hyperplanes split the spaces they are embedded in into three distinct parts:
037 * the hyperplane itself, a plus side and a minus side. However, since spherical
038 * space wraps around, a single oriented point is not sufficient to partition the space;
039 * any point could be classified as being on the plus or minus side of a hyperplane
040 * depending on the direction that the circle is traversed. The approach taken in this
041 * class to address this issue is to (1) define a second, implicit cut point at {@code 0pi} and
042 * (2) define the domain of hyperplane points (for partitioning purposes) to be the
043 * range {@code [0, 2pi)}. Each hyperplane then splits the space into the intervals
044 * {@code [0, x]} and {@code [x, 2pi)}, where {@code x} is the location of the hyperplane.
045 * One way to visualize this is to picture the circle as a cake that has already been
046 * cut at {@code 0pi}. Each hyperplane then specifies the location of the second
047 * cut of the cake, with the plus and minus sides being the pieces thus cut.
048 * </p>
049 *
050 * <p>Note that with the hyperplane partitioning rules described above, the hyperplane
051 * at {@code 0pi} is unique in that it has the entire space on one side (minus the hyperplane
052 * itself) and no points whatsoever on the other. This is very different from hyperplanes in
053 * Euclidean space, which always have infinitely many points on both sides.</p>
054 *
055 * <p>Instances of this class are guaranteed to be immutable.</p>
056 * @see CutAngles
057 */
058public final class CutAngle extends AbstractHyperplane<Point1S> {
059    /** Hyperplane location as a point. */
060    private final Point1S point;
061
062    /** Hyperplane direction. */
063    private final boolean positiveFacing;
064
065    /** Simple constructor.
066     * @param point location of the hyperplane
067     * @param positiveFacing if true, the hyperplane will point in a positive angular
068     *      direction; otherwise, it will point in a negative direction
069     * @param precision precision context used to compare floating point values
070     */
071    CutAngle(final Point1S point, final boolean positiveFacing,
072            final Precision.DoubleEquivalence precision) {
073        super(precision);
074
075        this.point = point;
076        this.positiveFacing = positiveFacing;
077    }
078
079    /** Get the location of the hyperplane as a point.
080     * @return the hyperplane location as a point
081     * @see #getAzimuth()
082     */
083    public Point1S getPoint() {
084        return point;
085    }
086
087    /** Get the location of the hyperplane as a single value. This is
088     * equivalent to {@code cutAngle.getPoint().getAzimuth()}.
089     * @return the location of the hyperplane as a single value.
090     * @see #getPoint()
091     * @see Point1S#getAzimuth()
092     */
093    public double getAzimuth() {
094        return point.getAzimuth();
095    }
096
097    /** Get the location of the hyperplane as a single value, normalized
098     * to the range {@code [0, 2pi)}. This is equivalent to
099     * {@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}