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}