AbstractNSphere.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.geometry.euclidean;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.ToDoubleBiFunction;
import org.apache.commons.geometry.core.Embedding;
import org.apache.commons.geometry.core.Point;
import org.apache.commons.geometry.core.Region;
import org.apache.commons.geometry.core.RegionLocation;
import org.apache.commons.geometry.euclidean.oned.Vector1D;
import org.apache.commons.numbers.core.Precision;
/** Abstract base class representing an n-sphere, which is a generalization of the ordinary 3 dimensional
* sphere to arbitrary dimensions.
* @param <V> Vector implementation type
* @see <a href="https://wikipedia.org/wiki/N-sphere">N-sphere</a>
*/
public abstract class AbstractNSphere<V extends EuclideanVector<V>> implements Region<V> {
/** The center point of the n-sphere. */
private final V center;
/** The radius of the n-sphere. */
private final double radius;
/** Precision object used to perform floating point comparisons. */
private final Precision.DoubleEquivalence precision;
/** Construct a new instance from its component parts.
* @param center the center point of the n-sphere
* @param radius the radius of the n-sphere
* @param precision precision context used to perform floating point comparisons
* @throws IllegalArgumentException if center is not finite or radius is not finite or is
* less than or equal to zero as evaluated by the given precision context
*/
protected AbstractNSphere(final V center, final double radius, final Precision.DoubleEquivalence precision) {
if (!center.isFinite()) {
throw new IllegalArgumentException("Illegal center point: " + center);
}
if (!Double.isFinite(radius) || precision.lte(radius, 0.0)) {
throw new IllegalArgumentException("Illegal radius: " + radius);
}
this.center = center;
this.radius = radius;
this.precision = precision;
}
/** Get the center point of the n-sphere.
* @return the center of the n-sphere
*/
public V getCenter() {
return center;
}
/** Get the radius of the n-sphere.
* @return the radius of the n-sphere.
*/
public double getRadius() {
return radius;
}
/** Get the precision object used to perform floating point
* comparisons for this instance.
* @return the precision object for this instance
*/
public Precision.DoubleEquivalence getPrecision() {
return precision;
}
/** {@inheritDoc}
*
* <p>This method always returns {@code false}.</p>
*/
@Override
public boolean isFull() {
return false;
}
/** {@inheritDoc}
*
* <p>This method always returns {@code false}.</p>
*/
@Override
public boolean isEmpty() {
return false;
}
/** {@inheritDoc}
*
* <p>This method is an alias for {@link #getCenter()}.</p>
*/
@Override
public V getCentroid() {
return getCenter();
}
/** {@inheritDoc} */
@Override
public RegionLocation classify(final V pt) {
final double dist = ((Point<V>) center).distance(pt);
final int cmp = precision.compare(dist, radius);
if (cmp < 0) {
return RegionLocation.INSIDE;
} else if (cmp > 0) {
return RegionLocation.OUTSIDE;
}
return RegionLocation.BOUNDARY;
}
/** {@inheritDoc} */
@Override
public int hashCode() {
return Objects.hash(center, radius, precision);
}
/** {@inheritDoc} */
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
} else if (obj == null || !obj.getClass().equals(this.getClass())) {
return false;
}
final AbstractNSphere<?> other = (AbstractNSphere<?>) obj;
return Objects.equals(this.center, other.center) &&
Double.compare(this.radius, other.radius) == 0 &&
Objects.equals(this.getPrecision(), other.getPrecision());
}
/** {@inheritDoc} */
@Override
public String toString() {
final StringBuilder sb = new StringBuilder(30);
sb.append(this.getClass().getSimpleName())
.append("[center= ")
.append(center)
.append(", radius= ")
.append(radius)
.append(']');
return sb.toString();
}
/** Project the given point to the boundary of the n-sphere. If
* the given point is exactly equal to the n-sphere center, it is
* projected to the boundary in the direction of {@code defaultVector}.
* @param pt the point to project
* @param defaultVector the direction to project the point if it lies
* exactly at the center of the n-sphere
* @return the projected point
*/
protected V project(final V pt, final V defaultVector) {
V vec = center.vectorTo(pt);
if (vec.equals(vec.getZero())) {
// use the default project vector if the given point lies
// exactly_ on the center point
vec = defaultVector;
}
return vec.withNorm(radius).add(center);
}
/** Internal method to compute the intersections between a line and this instance. The returned list will
* contain either 0, 1, or 2 points.
* <ul>
* <li><strong>2 points</strong> - The line is a secant line and intersects the n-sphere at two
* distinct points. The points are ordered such that the first point in the list is the first point
* encountered when traveling in the direction of the line. (In other words, the points are ordered
* by increasing abscissa value.)
* </li>
* <li><strong>1 point</strong> - The line is a tangent line and only intersects the n-sphere at a
* single point (as evaluated by the n-sphere's precision context).
* </li>
* <li><strong>0 points</strong> - The line does not intersect the n-sphere.</li>
* </ul>
* @param <L> Line implementation type
* @param line line to intersect with the n-sphere
* @param abscissaFn function used to compute the abscissa value of a point on a line
* @param distanceFn function used to compute the smallest distance between a point
* and a line
* @return a list of intersection points between the given line and this n-sphere
*/
protected <L extends Embedding<V, Vector1D>> List<V> intersections(final L line,
final ToDoubleBiFunction<L, V> abscissaFn, final ToDoubleBiFunction<L, V> distanceFn) {
final double dist = distanceFn.applyAsDouble(line, center);
final int cmp = precision.compare(dist, radius);
if (cmp <= 0) {
// on the boundary or inside the n-sphere
final double abscissa = abscissaFn.applyAsDouble(line, center);
final double abscissaDelta = Math.sqrt((radius * radius) - (dist * dist));
final V p0 = line.toSpace(Vector1D.of(abscissa - abscissaDelta));
if (cmp < 0) {
// secant line => two intersections
final V p1 = line.toSpace(Vector1D.of(abscissa + abscissaDelta));
return Arrays.asList(p0, p1);
}
// tangent line => one intersection
return Collections.singletonList(p0);
}
// no intersections
return Collections.emptyList();
}
/** Internal method to compute the first intersection between a line and this instance.
* @param <L> Line implementation type
* @param line line to intersect with the n-sphere
* @param abscissaFn function used to compute the abscissa value of a point on a line
* @param distanceFn function used to compute the smallest distance between a point
* and a line
* @return the first intersection between the given line and this instance or null if
* no such intersection exists
*/
protected <L extends Embedding<V, Vector1D>> V firstIntersection(final L line,
final ToDoubleBiFunction<L, V> abscissaFn, final ToDoubleBiFunction<L, V> distanceFn) {
final double dist = distanceFn.applyAsDouble(line, center);
final int cmp = precision.compare(dist, radius);
if (cmp <= 0) {
// on the boundary or inside the n-sphere
final double abscissa = abscissaFn.applyAsDouble(line, center);
final double abscissaDelta = Math.sqrt((radius * radius) - (dist * dist));
return line.toSpace(Vector1D.of(abscissa - abscissaDelta));
}
return null;
}
}