Line3D.java
/*
* Copyright (C) 2012 Alberto Irurueta Carro (alberto@irurueta.com)
*
* Licensed 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 com.irurueta.geometry;
import com.irurueta.algebra.AlgebraException;
import com.irurueta.algebra.Matrix;
import com.irurueta.algebra.SingularValueDecomposer;
import com.irurueta.algebra.Utils;
import java.io.Serializable;
/**
* This class defines a lines in 3D space.
* A line in 3D space is defined as the intersection of two non-parallel planes.
*/
public class Line3D implements Serializable {
/**
* Positive threshold determine whether points lay inside (is locus) of a
* given line or not.
*/
public static final double DEFAULT_LOCUS_THRESHOLD = 1e-12;
/**
* Minimum allowed threshold
*/
public static final double MIN_THRESHOLD = 0.0;
/**
* Constant defining the size of vector that define the direction of a line
*/
private static final int INHOM_VECTOR_SIZE = 3;
/**
* 1st plane forming this 3D line.
*/
private Plane plane1;
/**
* 2nd plane forming this 3D line.
*/
private Plane plane2;
/**
* Constructor.
* Sets planes intersecting into this 3D line.
*
* @param plane1 1st plane.
* @param plane2 2nd plane.
* @throws CoincidentPlanesException Raised if provided planes are
* coincident and hence never intersect. Notice that parallel planes are
* not coincident and they intersect at infinity.
*/
public Line3D(final Plane plane1, final Plane plane2) throws CoincidentPlanesException {
setPlanes(plane1, plane2);
}
/**
* Constructor.
* Builds a 3D line passing through provided 2 points.
*
* @param point1 1st point.
* @param point2 2nd point.
* @throws CoincidentPointsException Raised if provided points are
* considered to be equal.
*/
public Line3D(final Point3D point1, final Point3D point2) throws CoincidentPointsException {
setPlanesFromPoints(point1, point2);
}
/**
* Determines whether provided planes are coincident.
* Two planes are considered coincident if they are equal up to scale.
*
* @param plane1 1st plane.
* @param plane2 2nd plane.
* @return True if planes are coincident, false otherwise.
*/
public static boolean areCoincidentPlanes(final Plane plane1, final Plane plane2) {
// normalize planes to increase accuracy
plane1.normalize();
plane2.normalize();
try {
final var m = new Matrix(2, Plane.PLANE_NUMBER_PARAMS);
m.setElementAt(0, 0, plane1.getA());
m.setElementAt(0, 1, plane1.getB());
m.setElementAt(0, 2, plane1.getC());
m.setElementAt(0, 3, plane1.getD());
m.setElementAt(1, 0, plane2.getA());
m.setElementAt(1, 1, plane2.getB());
m.setElementAt(1, 2, plane2.getC());
m.setElementAt(1, 3, plane2.getD());
// check that matrix rank is 2 (and its nullity is also 2), in that
// case its right null-space has dimension two, which is linear
// combination of two 3D points (a line in 3D), and line is defined.
// Note that rank cannot be greater than 2 because matrix m has only
// 2 rows.
// If rank is smaller than 2, then the 2 planes are parallel, and the
// null-space has at least dimension 3, which is a perpendicular
// plane or the whole space (Depending whether nullity is 3 or 4)
return Utils.rank(m) < 2;
} catch (final AlgebraException e) {
// if for numerical reasons it cannot be determined whether planes
// are parallel, it will be assumed the worst case, which is that
// planes are parallel, which is something common for very large
// values or planes close at infinity
return true;
}
}
/**
* Sets intersecting planes for this 3D line.
* The intersection of provided planes will determine this 3D line.
*
* @param plane1 1st plane.
* @param plane2 2nd plane.
* @throws CoincidentPlanesException Raised if provided planes are
* coincident. Notice that parallel planes are not coincident and they
* intersect at infinity.
*/
public final void setPlanes(final Plane plane1, final Plane plane2) throws CoincidentPlanesException {
if (areCoincidentPlanes(plane1, plane2)) {
throw new CoincidentPlanesException();
}
this.plane1 = plane1;
this.plane2 = plane2;
}
/**
* Sets planes of this 3D line so that it passes through provided points.
*
* @param point1 1st point.
* @param point2 2nd point.
* @throws CoincidentPointsException Raised if provided points are equal
* and hence a line cannot be determined.
*/
public final void setPlanesFromPoints(final Point3D point1, final Point3D point2) throws CoincidentPointsException {
// build matrix containing director vector on a row
try {
final var m = new Matrix(1, INHOM_VECTOR_SIZE);
m.setElementAt(0, 0, point2.getInhomX() - point1.getInhomX());
m.setElementAt(0, 1, point2.getInhomY() - point1.getInhomY());
m.setElementAt(0, 2, point2.getInhomZ() - point1.getInhomZ());
// m matrix will have rank 1 and nullity 2. The null-space will be
// formed by two vectors perpendicular to the director vector
final var decomposer = new SingularValueDecomposer(m);
decomposer.decompose();
// check that points are not coincident, and hence all the values of
// matrix m are not zero
if (decomposer.getRank() < 1) {
throw new CoincidentPointsException();
}
// last two columns of V contains director vectors of plane1 and
// plane2
final var v = decomposer.getV();
final var directorVector1 = v.getSubmatrixAsArray(0, 1, 2, 1);
final var directorVector2 = v.getSubmatrixAsArray(0, 2, 2, 2);
plane1 = new Plane(point1, directorVector1);
plane2 = new Plane(point1, directorVector2);
} catch (final AlgebraException e) {
throw new CoincidentPointsException(e);
}
}
/**
* Returns 1st plane determining this 3D line.
* The intersection of plane1 and plane2 determines this 3D line.
*
* @return 1st plane.
*/
public Plane getPlane1() {
return plane1;
}
/**
* Returns 2nd plane determining this 3D line.
* The intersection of plane1 and plane2 determines this 3D line.
*
* @return 2nd plane.
*/
public Plane getPlane2() {
return plane2;
}
/**
* Determines if provided point is locus of this 3D line up to provided
* threshold.
*
* @param point Point to be checked.
* @param threshold Threshold to determine if provided point is locus or
* not. This should usually be a small value.
* @return True if provided point belongs to this 3D line, false otherwise.
* @throws IllegalArgumentException Raised if provided threshold is negative.
*/
public boolean isLocus(final Point3D point, final double threshold) {
return plane1.isLocus(point, threshold) && plane2.isLocus(point, threshold);
}
/**
* Raised if provided point is locus of this 3D line.
*
* @param point Point to be checked.
* @return True if provided point belongs to this 3D line, false otherwise.
*/
public boolean isLocus(final Point3D point) {
return isLocus(point, DEFAULT_LOCUS_THRESHOLD);
}
/**
* Returns shortest distance of provided point to this 3D line.
* The shortest distance is obtained in perpendicular direction of this
* line.
*
* @param point Point to be checked.
* @return Shortest distance of provided point to this 3D line.
*/
public double getDistance(final Point3D point) {
final var closestPoint = getClosestPoint(point);
return point.distanceTo(closestPoint);
}
/**
* Returns closest point belonging to this 3D line respect provided point.
*
* @param point Point to be checked.
* @return Closest point belonging to this 3D line respect provided point.
*/
public Point3D getClosestPoint(final Point3D point) {
return getClosestPoint(point, DEFAULT_LOCUS_THRESHOLD);
}
/**
* Returns closest point belonging to this 3D line respect provided point
* up to provided threshold.
*
* @param point Point to be checked.
* @param threshold Threshold to determine the closest point.
* @return Closest point belonging to this 3D line respect provided point.
* @throws IllegalArgumentException Raised if provided threshold is negative.
*/
public Point3D getClosestPoint(final Point3D point, final double threshold) {
final var result = Point3D.create();
closestPoint(point, result, threshold);
return result;
}
/**
* Computes closest point belonging to this 3D line respect provided point
* and stores the result in provided instance.
*
* @param point Point to be checked.
* @param result Instance where computed point will be stored.
*/
public void closestPoint(final Point3D point, final Point3D result) {
closestPoint(point, result, DEFAULT_LOCUS_THRESHOLD);
}
/**
* Computes closest point belonging to this 3D line respect provided point
* up to provided threshold and stores the result in provided instance.
*
* @param point Point to be checked.
* @param result Instance where computed point will be stored.
* @param threshold Threshold to determine the closest point.
* @throws IllegalArgumentException Raised if provided threshold is negative.
*/
public void closestPoint(final Point3D point, final Point3D result, final double threshold) {
if (threshold < MIN_THRESHOLD) {
throw new IllegalArgumentException();
}
// normalize to increase accuracy
point.normalize();
// compute director vector perpendicular to director vectors of plane1
// and plane2, and use it along provided point to set a 3rd plane.
// Using plane1, plane2 and the 3rd plane, we can get their intersection
// to obtain a point which will be locus of this line3 and will be
// located at shortest distance of provided point to this line3.
// This is a plane having as director vector this line 3D and passing
// through provided point
final var p = new Plane(point, getDirection());
try {
intersection(p, result);
} catch (final NoIntersectionException ignore) {
// never happens
}
}
/**
* Normalize the planes forming this 3D line.
*/
public void normalize() {
plane1.normalize();
plane2.normalize();
}
/**
* Determines whether the planes forming this 3D line are normalized or not.
*
* @return True if planes forming this 3D line are normalized, false
* otherwise.
*/
public boolean isNormalized() {
return plane1.isNormalized() && plane2.isNormalized();
}
/**
* Returns array containing vector that indicates the direction of this 3D
* line.
*
* @return Returns vector indicating the direction of this 3D line.
*/
public double[] getDirection() {
try {
return Utils.crossProduct(plane1.getDirectorVector(), plane2.getDirectorVector());
} catch (final AlgebraException ignore) {
return null;
}
}
/**
* Returns point where provided point intersects this 3D line.
*
* @param plane Plane to intersect this 3D line.
* @return Point where provided point intersects this 3D line.
* @throws NoIntersectionException Raised if provided plane does not
* intersect this 3D line.
*/
public Point3D getIntersection(final Plane plane) throws NoIntersectionException {
final var result = Point3D.create();
intersection(plane, result);
return result;
}
/**
* Computes point where provided point intersects this 3D line and stores
* the result in provided instance.
*
* @param plane Plane to intersect this 3D line.
* @param result Instance where computes point will be stored.
* @throws NoIntersectionException Raised if provided plane does not
* intersect this 3D line.
*/
public void intersection(final Plane plane, final Point3D result) throws NoIntersectionException {
// use plane1, plane2 and provided plane to find an intersection
plane.intersection(plane1, plane2, result);
}
}