blob: d1d5b2f7e95c0d8ee2af1e2e692ea4f829e98e1d [file] [log] [blame]
/*
* 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.threed;
import java.util.Objects;
import org.apache.commons.geometry.core.Embedding;
import org.apache.commons.geometry.core.Transform;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
import org.apache.commons.geometry.euclidean.oned.AffineTransformMatrix1D;
import org.apache.commons.geometry.euclidean.oned.Interval;
import org.apache.commons.geometry.euclidean.oned.Vector1D;
/** Class representing a line in 3D space.
*
* <p>Instances of this class are guaranteed to be immutable.</p>
*/
public final class Line3D implements Embedding<Vector3D, Vector1D> {
/** Line point closest to the origin. */
private final Vector3D origin;
/** Line direction. */
private final Vector3D direction;
/** Precision context used to compare floating point numbers. */
private final DoublePrecisionContext precision;
/** Simple constructor.
* @param origin the origin of the line, meaning the point on the line closest to the origin of the
* 3D space
* @param direction the direction of the line
* @param precision precision context used to compare floating point numbers
*/
private Line3D(final Vector3D origin, final Vector3D direction, final DoublePrecisionContext precision) {
this.origin = origin;
this.direction = direction;
this.precision = precision;
}
/** Get the line point closest to the origin.
* @return line point closest to the origin
*/
public Vector3D getOrigin() {
return origin;
}
/** Get the normalized direction vector.
* @return normalized direction vector
*/
public Vector3D getDirection() {
return direction;
}
/** Get the object used to determine floating point equality for this instance.
* @return the floating point precision context for the instance
*/
public DoublePrecisionContext getPrecision() {
return precision;
}
/** Return a line containing the same points as this instance but pointing
* in the opposite direction.
* @return an instance containing the same points but pointing in the opposite
* direction
*/
public Line3D reverse() {
return new Line3D(origin, direction.negate(), precision);
}
/** Transform this instance.
* @param transform object used to transform the instance
* @return a transformed instance
*/
public Line3D transform(final Transform<Vector3D> transform) {
final Vector3D p1 = transform.apply(origin);
final Vector3D p2 = transform.apply(origin.add(direction));
return fromPoints(p1, p2, precision);
}
/** Get an object containing the current line transformed by the argument along with a
* 1D transform that can be applied to subspace points. The subspace transform transforms
* subspace points such that their 3D location in the transformed line is the same as their
* 3D location in the original line after the 3D transform is applied. For example, consider
* the code below:
* <pre>
* SubspaceTransform st = line.subspaceTransform(transform);
*
* Vector1D subPt = Vector1D.of(1);
*
* Vector3D a = transform.apply(line.toSpace(subPt)); // transform in 3D space
* Vector3D b = st.getLine().toSpace(st.getTransform().apply(subPt)); // transform in 1D space
* </pre>
* At the end of execution, the points {@code a} (which was transformed using the original
* 3D transform) and {@code b} (which was transformed in 1D using the subspace transform)
* are equivalent.
*
* @param transform the transform to apply to this instance
* @return an object containing the transformed line along with a transform that can be applied
* to subspace points
* @see #transform(Transform)
*/
public SubspaceTransform subspaceTransform(final Transform<Vector3D> transform) {
final Vector3D p1 = transform.apply(origin);
final Vector3D p2 = transform.apply(origin.add(direction));
final Line3D tLine = fromPoints(p1, p2, precision);
final Vector1D tSubspaceOrigin = tLine.toSubspace(p1);
final Vector1D tSubspaceDirection = tSubspaceOrigin.vectorTo(tLine.toSubspace(p2));
final double translation = tSubspaceOrigin.getX();
final double scale = tSubspaceDirection.getX();
final AffineTransformMatrix1D subspaceTransform = AffineTransformMatrix1D.of(scale, translation);
return new SubspaceTransform(tLine, subspaceTransform);
}
/** Get the abscissa of the given point on the line. The abscissa represents
* the distance the projection of the point on the line is from the line's
* origin point (the point on the line closest to the origin of the
* 2D space). Abscissa values increase in the direction of the line. This method
* is exactly equivalent to {@link #toSubspace(Vector3D)} except that this method
* returns a double instead of a {@link Vector1D}.
* @param point point to compute the abscissa for
* @return abscissa value of the point
* @see #toSubspace(Vector3D)
*/
public double abscissa(final Vector3D point) {
return point.subtract(origin).dot(direction);
}
/** Get one point from the line.
* @param abscissa desired abscissa for the point
* @return one point belonging to the line, at specified abscissa
*/
public Vector3D pointAt(final double abscissa) {
return Vector3D.linearCombination(1.0, origin, abscissa, direction);
}
/** {@inheritDoc} */
@Override
public Vector1D toSubspace(Vector3D pt) {
return Vector1D.of(abscissa(pt));
}
/** {@inheritDoc} */
@Override
public Vector3D toSpace(Vector1D pt) {
return toSpace(pt.getX());
}
/** Get the 3 dimensional point at the given abscissa position
* on the line.
* @param abscissa location on the line
* @return the 3 dimensional point at the given abscissa position
* on the line
*/
public Vector3D toSpace(final double abscissa) {
return pointAt(abscissa);
}
/** Check if the instance is similar to another line.
* <p>Lines are considered similar if they contain the same
* points. This does not mean they are equal since they can have
* opposite directions.</p>
* @param line line to which instance should be compared
* @return true if the lines are similar
*/
public boolean isSimilarTo(final Line3D line) {
final double angle = direction.angle(line.direction);
return (precision.eqZero(angle) || precision.eq(Math.abs(angle), Math.PI)) &&
contains(line.origin);
}
/** Check if the instance contains a point.
* @param pt point to check
* @return true if p belongs to the line
*/
public boolean contains(final Vector3D pt) {
return precision.eqZero(distance(pt));
}
/** Compute the distance between the instance and a point.
* @param pt to check
* @return distance between the instance and the point
*/
public double distance(final Vector3D pt) {
final Vector3D delta = pt.subtract(origin);
final Vector3D orthogonal = delta.reject(direction);
return orthogonal.norm();
}
/** Compute the shortest distance between the instance and another line.
* @param line line to check against the instance
* @return shortest distance between the instance and the line
*/
public double distance(final Line3D line) {
final Vector3D normal = direction.cross(line.direction);
final double norm = normal.norm();
if (precision.eqZero(norm)) {
// the lines are parallel
return distance(line.origin);
}
// signed separation of the two parallel planes that contains the lines
final double offset = line.origin.subtract(origin).dot(normal) / norm;
return Math.abs(offset);
}
/** Compute the point of the instance closest to another line.
* @param line line to check against the instance
* @return point of the instance closest to another line
*/
public Vector3D closest(final Line3D line) {
final double cos = direction.dot(line.direction);
final double n = 1 - cos * cos;
if (precision.eqZero(n)) {
// the lines are parallel
return origin;
}
final Vector3D delta = line.origin.subtract(origin);
final double a = delta.dot(direction);
final double b = delta.dot(line.direction);
return Vector3D.linearCombination(1, origin, (a - (b * cos)) / n, direction);
}
/** Get the intersection point of the instance and another line.
* @param line other line
* @return intersection point of the instance and the other line
* or null if there are no intersection points
*/
public Vector3D intersection(final Line3D line) {
final Vector3D closestPt = closest(line);
return line.contains(closestPt) ? closestPt : null;
}
/** Return a new infinite segment representing the entire line.
* @return a new infinite segment representing the entire line
*/
public Segment3D span() {
return Segment3D.fromInterval(this, Interval.full());
}
/** Create a new line segment from the given interval.
* @param interval interval representing the 1D region for the line segment
* @return a new line segment on this line
*/
public Segment3D segment(final Interval interval) {
return Segment3D.fromInterval(this, interval);
}
/** Create a new line segment from the given interval.
* @param a first 1D location for the interval
* @param b second 1D location for the interval
* @return a new line segment on this line
*/
public Segment3D segment(final double a, final double b) {
return Segment3D.fromInterval(this, a, b);
}
/** Create a new line segment between the projections of the two
* given points onto this line.
* @param a first point
* @param b second point
* @return a new line segment on this line
*/
public Segment3D segment(final Vector3D a, final Vector3D b) {
return Segment3D.fromInterval(this, toSubspace(a), toSubspace(b));
}
/** Create a new line segment that starts at infinity and continues along
* the line up to the projection of the given point.
* @param pt point defining the end point of the line segment; the end point
* is equal to the projection of this point onto the line
* @return a new, half-open line segment
*/
public Segment3D segmentTo(final Vector3D pt) {
return segment(Double.NEGATIVE_INFINITY, toSubspace(pt).getX());
}
/** Create a new line segment that starts at the projection of the given point
* and continues in the direction of the line to infinity, similar to a ray.
* @param pt point defining the start point of the line segment; the start point
* is equal to the projection of this point onto the line
* @return a new, half-open line segment
*/
public Segment3D segmentFrom(final Vector3D pt) {
return segment(toSubspace(pt).getX(), Double.POSITIVE_INFINITY);
}
/** Create a new, empty subline based on this line.
* @return a new, empty subline based on this line
*/
public SubLine3D subline() {
return new SubLine3D(this);
}
/** {@inheritDoc} */
@Override
public int hashCode() {
return Objects.hash(origin, direction, precision);
}
/** {@inheritDoc} */
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Line3D)) {
return false;
}
Line3D other = (Line3D) obj;
return this.origin.equals(other.origin) &&
this.direction.equals(other.direction) &&
this.precision.equals(other.precision);
}
/** {@inheritDoc} */
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName())
.append("[origin= ")
.append(origin)
.append(", direction= ")
.append(direction)
.append("]");
return sb.toString();
}
/** Create a new line instance from two points that lie on the line. The line
* direction points from the first point to the second point.
* @param p1 first point on the line
* @param p2 second point on the line
* @param precision floating point precision context
* @return a new line instance that contains both of the given point and that has
* a direction going from the first point to the second point
* @throws org.apache.commons.geometry.core.exception.IllegalNormException if the points lie too close to
* create a non-zero direction vector
*/
public static Line3D fromPoints(final Vector3D p1, final Vector3D p2,
final DoublePrecisionContext precision) {
return fromPointAndDirection(p1, p1.directionTo(p2), precision);
}
/** Create a new line instance from a point and a direction.
* @param pt a point lying on the line
* @param direction the direction of the line
* @param precision floating point precision context
* @return a new line instance that contains the given point and points in the
* given direction
* @throws org.apache.commons.geometry.core.exception.IllegalNormException if the direction cannot be normalized
*/
public static Line3D fromPointAndDirection(final Vector3D pt, final Vector3D direction,
final DoublePrecisionContext precision) {
final Vector3D normDirection = direction.normalize();
final Vector3D origin = pt.reject(normDirection);
return new Line3D(origin, normDirection, precision);
}
/** Class containing a transformed line instance along with a subspace (1D) transform. The subspace
* transform produces the equivalent of the 3D transform in 1D.
*/
public static final class SubspaceTransform {
/** The transformed line. */
private final Line3D line;
/** The subspace transform instance. */
private final AffineTransformMatrix1D transform;
/** Simple constructor.
* @param line the transformed line
* @param transform 1D transform that can be applied to subspace points
*/
public SubspaceTransform(final Line3D line, final AffineTransformMatrix1D transform) {
this.line = line;
this.transform = transform;
}
/** Get the transformed line instance.
* @return the transformed line instance
*/
public Line3D getLine() {
return line;
}
/** Get the 1D transform that can be applied to subspace points. This transform can be used
* to perform the equivalent of the 3D transform in 1D space.
* @return the subspace transform instance
*/
public AffineTransformMatrix1D getTransform() {
return transform;
}
}
}