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.euclidean.threed.line;
018
019import java.text.MessageFormat;
020import java.util.Objects;
021
022import org.apache.commons.geometry.core.Embedding;
023import org.apache.commons.geometry.core.Transform;
024import org.apache.commons.geometry.euclidean.oned.AffineTransformMatrix1D;
025import org.apache.commons.geometry.euclidean.oned.Vector1D;
026import org.apache.commons.geometry.euclidean.threed.Vector3D;
027import org.apache.commons.numbers.core.Precision;
028
029/** Class representing a line in 3D space.
030 *
031 * <p>Instances of this class are guaranteed to be immutable.</p>
032 * @see Lines3D
033 */
034public final class Line3D implements Embedding<Vector3D, Vector1D> {
035
036    /** Format string for creating line string representations. */
037    static final String TO_STRING_FORMAT = "{0}[origin= {1}, direction= {2}]";
038
039    /** Line point closest to the origin. */
040    private final Vector3D origin;
041
042    /** Line direction. */
043    private final Vector3D direction;
044
045    /** Precision context used to compare floating point numbers. */
046    private final Precision.DoubleEquivalence precision;
047
048    /** Simple constructor.
049     * @param origin the origin of the line, meaning the point on the line closest to the origin of the
050     *      3D space
051     * @param direction the direction of the line
052     * @param precision precision context used to compare floating point numbers
053     */
054    Line3D(final Vector3D origin, final Vector3D direction, final Precision.DoubleEquivalence precision) {
055        this.origin = origin;
056        this.direction = direction;
057        this.precision = precision;
058    }
059
060    /** Get the line point closest to the origin.
061     * @return line point closest to the origin
062     */
063    public Vector3D getOrigin() {
064        return origin;
065    }
066
067    /** Get the normalized direction vector.
068     * @return normalized direction vector
069     */
070    public Vector3D getDirection() {
071        return direction;
072    }
073
074    /** Get the object used to determine floating point equality for this instance.
075     * @return the floating point precision context for the instance
076     */
077    public Precision.DoubleEquivalence getPrecision() {
078        return precision;
079    }
080
081    /** Return a line containing the same points as this instance but pointing
082     * in the opposite direction.
083     * @return an instance containing the same points but pointing in the opposite
084     *      direction
085     */
086    public Line3D reverse() {
087        return new Line3D(origin, direction.negate(), precision);
088    }
089
090    /** Transform this instance.
091     * @param transform object used to transform the instance
092     * @return a transformed instance
093     */
094    public Line3D transform(final Transform<Vector3D> transform) {
095        final Vector3D p1 = transform.apply(origin);
096        final Vector3D p2 = transform.apply(origin.add(direction));
097
098        return Lines3D.fromPoints(p1, p2, precision);
099    }
100
101    /** Get an object containing the current line transformed by the argument along with a
102     * 1D transform that can be applied to subspace points. The subspace transform transforms
103     * subspace points such that their 3D location in the transformed line is the same as their
104     * 3D location in the original line after the 3D transform is applied. For example, consider
105     * the code below:
106     * <pre>
107     *      SubspaceTransform st = line.subspaceTransform(transform);
108     *
109     *      Vector1D subPt = Vector1D.of(1);
110     *
111     *      Vector3D a = transform.apply(line.toSpace(subPt)); // transform in 3D space
112     *      Vector3D b = st.getLine().toSpace(st.getTransform().apply(subPt)); // transform in 1D space
113     * </pre>
114     * At the end of execution, the points {@code a} (which was transformed using the original
115     * 3D transform) and {@code b} (which was transformed in 1D using the subspace transform)
116     * are equivalent.
117     *
118     * @param transform the transform to apply to this instance
119     * @return an object containing the transformed line along with a transform that can be applied
120     *      to subspace points
121     * @see #transform(Transform)
122     */
123    public SubspaceTransform subspaceTransform(final Transform<Vector3D> transform) {
124        final Vector3D p1 = transform.apply(origin);
125        final Vector3D p2 = transform.apply(origin.add(direction));
126
127        final Line3D tLine = Lines3D.fromPoints(p1, p2, precision);
128
129        final Vector1D tSubspaceOrigin = tLine.toSubspace(p1);
130        final Vector1D tSubspaceDirection = tSubspaceOrigin.vectorTo(tLine.toSubspace(p2));
131
132        final double translation = tSubspaceOrigin.getX();
133        final double scale = tSubspaceDirection.getX();
134
135        final AffineTransformMatrix1D subspaceTransform = AffineTransformMatrix1D.of(scale, translation);
136
137        return new SubspaceTransform(tLine, subspaceTransform);
138    }
139
140    /** Get the abscissa of the given point on the line. The abscissa represents
141     * the distance the projection of the point on the line is from the line's
142     * origin point (the point on the line closest to the origin of the
143     * 2D space). Abscissa values increase in the direction of the line. This method
144     * is exactly equivalent to {@link #toSubspace(Vector3D)} except that this method
145     * returns a double instead of a {@link Vector1D}.
146     * @param pt point to compute the abscissa for
147     * @return abscissa value of the point
148     * @see #toSubspace(Vector3D)
149     */
150    public double abscissa(final Vector3D pt) {
151        return pt.subtract(origin).dot(direction);
152    }
153
154    /** Get one point from the line.
155     * @param abscissa desired abscissa for the point
156     * @return one point belonging to the line, at specified abscissa
157     */
158    public Vector3D pointAt(final double abscissa) {
159        return Vector3D.Sum.of(origin)
160                .addScaled(abscissa, direction)
161                .get();
162    }
163
164    /** {@inheritDoc} */
165    @Override
166    public Vector1D toSubspace(final Vector3D pt) {
167        return Vector1D.of(abscissa(pt));
168    }
169
170    /** {@inheritDoc} */
171    @Override
172    public Vector3D toSpace(final Vector1D pt) {
173        return toSpace(pt.getX());
174    }
175
176    /** Get the 3 dimensional point at the given abscissa position
177     * on the line.
178     * @param abscissa location on the line
179     * @return the 3 dimensional point at the given abscissa position
180     *      on the line
181     */
182    public Vector3D toSpace(final double abscissa) {
183        return pointAt(abscissa);
184    }
185
186    /** Check if the instance is similar to another line.
187     * <p>Lines are considered similar if they contain the same
188     * points. This does not mean they are equal since they can have
189     * opposite directions.</p>
190     * @param line line to which instance should be compared
191     * @return true if the lines are similar
192     */
193    public boolean isSimilarTo(final Line3D line) {
194        final double angle = direction.angle(line.direction);
195        return (precision.eqZero(angle) || precision.eq(Math.abs(angle), Math.PI)) &&
196                contains(line.origin);
197    }
198
199    /** Check if the instance contains a point.
200     * @param pt point to check
201     * @return true if p belongs to the line
202     */
203    public boolean contains(final Vector3D pt) {
204        return precision.eqZero(distance(pt));
205    }
206
207    /** Compute the distance between the instance and a point.
208     * @param pt to check
209     * @return distance between the instance and the point
210     */
211    public double distance(final Vector3D pt) {
212        final Vector3D delta = pt.subtract(origin);
213        final Vector3D orthogonal = delta.reject(direction);
214
215        return orthogonal.norm();
216    }
217
218    /** Compute the shortest distance between the instance and another line.
219     * @param line line to check against the instance
220     * @return shortest distance between the instance and the line
221     */
222    public double distance(final Line3D line) {
223
224        final Vector3D normal = direction.cross(line.direction);
225        final double norm = normal.norm();
226
227        if (precision.eqZero(norm)) {
228            // the lines are parallel
229            return distance(line.origin);
230        }
231
232        // signed separation of the two parallel planes that contains the lines
233        final double offset = line.origin.subtract(origin).dot(normal) / norm;
234
235        return Math.abs(offset);
236    }
237
238    /** Compute the point of the instance closest to another line.
239     * @param line line to check against the instance
240     * @return point of the instance closest to another line
241     */
242    public Vector3D closest(final Line3D line) {
243
244        final double cos = direction.dot(line.direction);
245        final double n = 1 - cos * cos;
246
247        if (precision.eqZero(n)) {
248            // the lines are parallel
249            return origin;
250        }
251
252        final Vector3D delta = line.origin.subtract(origin);
253        final double a = delta.dot(direction);
254        final double b = delta.dot(line.direction);
255
256        return Vector3D.Sum.of(origin)
257                .addScaled((a - (b * cos)) / n, direction)
258                .get();
259    }
260
261    /** Get the intersection point of the instance and another line.
262     * @param line other line
263     * @return intersection point of the instance and the other line
264     * or null if there are no intersection points
265     */
266    public Vector3D intersection(final Line3D line) {
267        final Vector3D closestPt = closest(line);
268        return line.contains(closestPt) ? closestPt : null;
269    }
270
271    /** Return a new infinite line subset representing the entire line.
272     * @return a new infinite line subset representing the entire line
273     * @see Lines3D#span(Line3D)
274     */
275    public LineConvexSubset3D span() {
276        return Lines3D.span(this);
277    }
278
279    /** Create a new line segment from the given 1D interval. The returned line
280     * segment consists of all points between the two locations, regardless of the order the
281     * arguments are given.
282     * @param a first 1D location for the interval
283     * @param b second 1D location for the interval
284     * @return a new line segment on this line
285     * @throws IllegalArgumentException if either of the locations is NaN or infinite
286     * @see Lines3D#segmentFromLocations(Line3D, double, double)
287     */
288    public Segment3D segment(final double a, final double b) {
289        return Lines3D.segmentFromLocations(this, a, b);
290    }
291
292    /** Create a new line segment from two points. The returned segment represents all points on this line
293     * between the projected locations of {@code a} and {@code b}. The points may be given in any order.
294     * @param a first point
295     * @param b second point
296     * @return a new line segment on this line
297     * @throws IllegalArgumentException if either point contains NaN or infinite coordinate values
298     * @see Lines3D#segmentFromPoints(Line3D, Vector3D, Vector3D)
299     */
300    public Segment3D segment(final Vector3D a, final Vector3D b) {
301        return Lines3D.segmentFromPoints(this, a, b);
302    }
303
304    /** Create a new line convex subset that starts at infinity and continues along
305     * the line up to the projection of the given end point.
306     * @param endPoint point defining the end point of the line subset; the end point
307     *      is equal to the projection of this point onto the line
308     * @return a new, half-open line subset that ends at the given point
309     * @throws IllegalArgumentException if any coordinate in {@code endPoint} is NaN or infinite
310     * @see Lines3D#reverseRayFromPoint(Line3D, Vector3D)
311     */
312    public ReverseRay3D reverseRayTo(final Vector3D endPoint) {
313        return Lines3D.reverseRayFromPoint(this, endPoint);
314    }
315
316    /** Create a new line convex subset that starts at infinity and continues along
317     * the line up to the given 1D location.
318     * @param endLocation the 1D location of the end of the half-line
319     * @return a new, half-open line subset that ends at the given 1D location
320     * @throws IllegalArgumentException if {@code endLocation} is NaN or infinite
321     * @see Lines3D#reverseRayFromLocation(Line3D, double)
322     */
323    public ReverseRay3D reverseRayTo(final double endLocation) {
324        return Lines3D.reverseRayFromLocation(this, endLocation);
325    }
326
327    /** Create a new ray instance that starts at the projection of the given point
328     * and continues in the direction of the line to infinity.
329     * @param startPoint point defining the start point of the ray; the start point
330     *      is equal to the projection of this point onto the line
331     * @return a ray starting at the projected point and extending along this line
332     *      to infinity
333     * @throws IllegalArgumentException if any coordinate in {@code startPoint} is NaN or infinite
334     * @see Lines3D#rayFromPoint(Line3D, Vector3D)
335     */
336    public Ray3D rayFrom(final Vector3D startPoint) {
337        return Lines3D.rayFromPoint(this, startPoint);
338    }
339
340    /** Create a new ray instance that starts at the given 1D location and continues in
341     * the direction of the line to infinity.
342     * @param startLocation 1D location defining the start point of the ray
343     * @return a ray starting at the given 1D location and extending along this line
344     *      to infinity
345     * @throws IllegalArgumentException if {@code startLocation} is NaN or infinite
346     * @see Lines3D#rayFromLocation(Line3D, double)
347     */
348    public Ray3D rayFrom(final double startLocation) {
349        return Lines3D.rayFromLocation(this, startLocation);
350    }
351
352    /** Return true if this instance should be considered equivalent to the argument, using the
353     * given precision context for comparison. Instances are considered equivalent if they have
354     * equivalent {@code origin}s and {@code direction}s.
355     * @param other the point to compare with
356     * @param ctx precision context to use for the comparison
357     * @return true if this instance should be considered equivalent to the argument
358     * @see Vector3D#eq(Vector3D, Precision.DoubleEquivalence)
359     */
360    public boolean eq(final Line3D other, final Precision.DoubleEquivalence ctx) {
361        return getOrigin().eq(other.getOrigin(), ctx) &&
362                getDirection().eq(other.getDirection(), ctx);
363    }
364
365    /** {@inheritDoc} */
366    @Override
367    public int hashCode() {
368        return Objects.hash(origin, direction, precision);
369    }
370
371    /** {@inheritDoc} */
372    @Override
373    public boolean equals(final Object obj) {
374        if (this == obj) {
375            return true;
376        }
377        if (!(obj instanceof Line3D)) {
378            return false;
379        }
380        final Line3D other = (Line3D) obj;
381        return this.origin.equals(other.origin) &&
382                this.direction.equals(other.direction) &&
383                this.precision.equals(other.precision);
384    }
385
386    /** {@inheritDoc} */
387    @Override
388    public String toString() {
389        return MessageFormat.format(TO_STRING_FORMAT,
390                getClass().getSimpleName(),
391                getOrigin(),
392                getDirection());
393    }
394
395    /** Class containing a transformed line instance along with a subspace (1D) transform. The subspace
396     * transform produces the equivalent of the 3D transform in 1D.
397     */
398    public static final class SubspaceTransform {
399        /** The transformed line. */
400        private final Line3D line;
401
402        /** The subspace transform instance. */
403        private final AffineTransformMatrix1D transform;
404
405        /** Simple constructor.
406         * @param line the transformed line
407         * @param transform 1D transform that can be applied to subspace points
408         */
409        public SubspaceTransform(final Line3D line, final AffineTransformMatrix1D transform) {
410            this.line = line;
411            this.transform = transform;
412        }
413
414        /** Get the transformed line instance.
415         * @return the transformed line instance
416         */
417        public Line3D getLine() {
418            return line;
419        }
420
421        /** Get the 1D transform that can be applied to subspace points. This transform can be used
422         * to perform the equivalent of the 3D transform in 1D space.
423         * @return the subspace transform instance
424         */
425        public AffineTransformMatrix1D getTransform() {
426            return transform;
427        }
428    }
429}