View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.geometry.euclidean.threed.line;
18  
19  import java.text.MessageFormat;
20  import java.util.Objects;
21  
22  import org.apache.commons.geometry.core.Embedding;
23  import org.apache.commons.geometry.core.Transform;
24  import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
25  import org.apache.commons.geometry.euclidean.oned.AffineTransformMatrix1D;
26  import org.apache.commons.geometry.euclidean.oned.Vector1D;
27  import org.apache.commons.geometry.euclidean.threed.Vector3D;
28  
29  /** Class representing a line in 3D space.
30   *
31   * <p>Instances of this class are guaranteed to be immutable.</p>
32   * @see Lines3D
33   */
34  public final class Line3D implements Embedding<Vector3D, Vector1D> {
35  
36      /** Format string for creating line string representations. */
37      static final String TO_STRING_FORMAT = "{0}[origin= {1}, direction= {2}]";
38  
39      /** Line point closest to the origin. */
40      private final Vector3D origin;
41  
42      /** Line direction. */
43      private final Vector3D direction;
44  
45      /** Precision context used to compare floating point numbers. */
46      private final DoublePrecisionContext precision;
47  
48      /** Simple constructor.
49       * @param origin the origin of the line, meaning the point on the line closest to the origin of the
50       *      3D space
51       * @param direction the direction of the line
52       * @param precision precision context used to compare floating point numbers
53       */
54      Line3D(final Vector3D origin, final Vector3D direction, final DoublePrecisionContext precision) {
55          this.origin = origin;
56          this.direction = direction;
57          this.precision = precision;
58      }
59  
60      /** Get the line point closest to the origin.
61       * @return line point closest to the origin
62       */
63      public Vector3D getOrigin() {
64          return origin;
65      }
66  
67      /** Get the normalized direction vector.
68       * @return normalized direction vector
69       */
70      public Vector3D getDirection() {
71          return direction;
72      }
73  
74      /** Get the object used to determine floating point equality for this instance.
75       * @return the floating point precision context for the instance
76       */
77      public DoublePrecisionContext getPrecision() {
78          return precision;
79      }
80  
81      /** Return a line containing the same points as this instance but pointing
82       * in the opposite direction.
83       * @return an instance containing the same points but pointing in the opposite
84       *      direction
85       */
86      public Line3D reverse() {
87          return new Line3D(origin, direction.negate(), precision);
88      }
89  
90      /** Transform this instance.
91       * @param transform object used to transform the instance
92       * @return a transformed instance
93       */
94      public Line3D transform(final Transform<Vector3D> transform) {
95          final Vector3D p1 = transform.apply(origin);
96          final Vector3D p2 = transform.apply(origin.add(direction));
97  
98          return Lines3D.fromPoints(p1, p2, precision);
99      }
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.linearCombination(1.0, origin, abscissa, direction);
160     }
161 
162     /** {@inheritDoc} */
163     @Override
164     public Vector1D toSubspace(final Vector3D pt) {
165         return Vector1D.of(abscissa(pt));
166     }
167 
168     /** {@inheritDoc} */
169     @Override
170     public Vector3D toSpace(final Vector1D pt) {
171         return toSpace(pt.getX());
172     }
173 
174     /** Get the 3 dimensional point at the given abscissa position
175      * on the line.
176      * @param abscissa location on the line
177      * @return the 3 dimensional point at the given abscissa position
178      *      on the line
179      */
180     public Vector3D toSpace(final double abscissa) {
181         return pointAt(abscissa);
182     }
183 
184     /** Check if the instance is similar to another line.
185      * <p>Lines are considered similar if they contain the same
186      * points. This does not mean they are equal since they can have
187      * opposite directions.</p>
188      * @param line line to which instance should be compared
189      * @return true if the lines are similar
190      */
191     public boolean isSimilarTo(final Line3D line) {
192         final double angle = direction.angle(line.direction);
193         return (precision.eqZero(angle) || precision.eq(Math.abs(angle), Math.PI)) &&
194                 contains(line.origin);
195     }
196 
197     /** Check if the instance contains a point.
198      * @param pt point to check
199      * @return true if p belongs to the line
200      */
201     public boolean contains(final Vector3D pt) {
202         return precision.eqZero(distance(pt));
203     }
204 
205     /** Compute the distance between the instance and a point.
206      * @param pt to check
207      * @return distance between the instance and the point
208      */
209     public double distance(final Vector3D pt) {
210         final Vector3D delta = pt.subtract(origin);
211         final Vector3D orthogonal = delta.reject(direction);
212 
213         return orthogonal.norm();
214     }
215 
216     /** Compute the shortest distance between the instance and another line.
217      * @param line line to check against the instance
218      * @return shortest distance between the instance and the line
219      */
220     public double distance(final Line3D line) {
221 
222         final Vector3D normal = direction.cross(line.direction);
223         final double norm = normal.norm();
224 
225         if (precision.eqZero(norm)) {
226             // the lines are parallel
227             return distance(line.origin);
228         }
229 
230         // signed separation of the two parallel planes that contains the lines
231         final double offset = line.origin.subtract(origin).dot(normal) / norm;
232 
233         return Math.abs(offset);
234     }
235 
236     /** Compute the point of the instance closest to another line.
237      * @param line line to check against the instance
238      * @return point of the instance closest to another line
239      */
240     public Vector3D closest(final Line3D line) {
241 
242         final double cos = direction.dot(line.direction);
243         final double n = 1 - cos * cos;
244 
245         if (precision.eqZero(n)) {
246             // the lines are parallel
247             return origin;
248         }
249 
250         final Vector3D delta = line.origin.subtract(origin);
251         final double a = delta.dot(direction);
252         final double b = delta.dot(line.direction);
253 
254         return Vector3D.linearCombination(1, origin, (a - (b * cos)) / n, direction);
255     }
256 
257     /** Get the intersection point of the instance and another line.
258      * @param line other line
259      * @return intersection point of the instance and the other line
260      * or null if there are no intersection points
261      */
262     public Vector3D intersection(final Line3D line) {
263         final Vector3D closestPt = closest(line);
264         return line.contains(closestPt) ? closestPt : null;
265     }
266 
267     /** Return a new infinite line subset representing the entire line.
268      * @return a new infinite line subset representing the entire line
269      * @see Lines3D#span(Line3D)
270      */
271     public LineConvexSubset3D span() {
272         return Lines3D.span(this);
273     }
274 
275     /** Create a new line segment from the given 1D interval. The returned line
276      * segment consists of all points between the two locations, regardless of the order the
277      * arguments are given.
278      * @param a first 1D location for the interval
279      * @param b second 1D location for the interval
280      * @return a new line segment on this line
281      * @throws IllegalArgumentException if either of the locations is NaN or infinite
282      * @see Lines3D#segmentFromLocations(Line3D, double, double)
283      */
284     public Segment3D segment(final double a, final double b) {
285         return Lines3D.segmentFromLocations(this, a, b);
286     }
287 
288     /** Create a new line segment from two points. The returned segment represents all points on this line
289      * between the projected locations of {@code a} and {@code b}. The points may be given in any order.
290      * @param a first point
291      * @param b second point
292      * @return a new line segment on this line
293      * @throws IllegalArgumentException if either point contains NaN or infinite coordinate values
294      * @see Lines3D#segmentFromPoints(Line3D, Vector3D, Vector3D)
295      */
296     public Segment3D segment(final Vector3D a, final Vector3D b) {
297         return Lines3D.segmentFromPoints(this, a, b);
298     }
299 
300     /** Create a new line convex subset that starts at infinity and continues along
301      * the line up to the projection of the given end point.
302      * @param endPoint point defining the end point of the line subset; the end point
303      *      is equal to the projection of this point onto the line
304      * @return a new, half-open line subset that ends at the given point
305      * @throws IllegalArgumentException if any coordinate in {@code endPoint} is NaN or infinite
306      * @see Lines3D#reverseRayFromPoint(Line3D, Vector3D)
307      */
308     public ReverseRay3D reverseRayTo(final Vector3D endPoint) {
309         return Lines3D.reverseRayFromPoint(this, endPoint);
310     }
311 
312     /** Create a new line convex subset that starts at infinity and continues along
313      * the line up to the given 1D location.
314      * @param endLocation the 1D location of the end of the half-line
315      * @return a new, half-open line subset that ends at the given 1D location
316      * @throws IllegalArgumentException if {@code endLocation} is NaN or infinite
317      * @see Lines3D#reverseRayFromLocation(Line3D, double)
318      */
319     public ReverseRay3D reverseRayTo(final double endLocation) {
320         return Lines3D.reverseRayFromLocation(this, endLocation);
321     }
322 
323     /** Create a new ray instance that starts at the projection of the given point
324      * and continues in the direction of the line to infinity.
325      * @param startPoint point defining the start point of the ray; the start point
326      *      is equal to the projection of this point onto the line
327      * @return a ray starting at the projected point and extending along this line
328      *      to infinity
329      * @throws IllegalArgumentException if any coordinate in {@code startPoint} is NaN or infinite
330      * @see Lines3D#rayFromPoint(Line3D, Vector3D)
331      */
332     public Ray3D rayFrom(final Vector3D startPoint) {
333         return Lines3D.rayFromPoint(this, startPoint);
334     }
335 
336     /** Create a new ray instance that starts at the given 1D location and continues in
337      * the direction of the line to infinity.
338      * @param startLocation 1D location defining the start point of the ray
339      * @return a ray starting at the given 1D location and extending along this line
340      *      to infinity
341      * @throws IllegalArgumentException if {@code startLocation} is NaN or infinite
342      * @see Lines3D#rayFromLocation(Line3D, double)
343      */
344     public Ray3D rayFrom(final double startLocation) {
345         return Lines3D.rayFromLocation(this, startLocation);
346     }
347 
348     /** Return true if this instance should be considered equivalent to the argument, using the
349      * given precision context for comparison. Instances are considered equivalent if they have
350      * equivalent {@code origin}s and {@code direction}s.
351      * @param other the point to compare with
352      * @param ctx precision context to use for the comparison
353      * @return true if this instance should be considered equivalent to the argument
354      * @see Vector3D#eq(Vector3D, DoublePrecisionContext)
355      */
356     public boolean eq(final Line3D other, final DoublePrecisionContext ctx) {
357         return getOrigin().eq(other.getOrigin(), ctx) &&
358                 getDirection().eq(other.getDirection(), ctx);
359     }
360 
361     /** {@inheritDoc} */
362     @Override
363     public int hashCode() {
364         return Objects.hash(origin, direction, precision);
365     }
366 
367     /** {@inheritDoc} */
368     @Override
369     public boolean equals(final Object obj) {
370         if (this == obj) {
371             return true;
372         }
373         if (!(obj instanceof Line3D)) {
374             return false;
375         }
376         final Line3D other = (Line3D) obj;
377         return this.origin.equals(other.origin) &&
378                 this.direction.equals(other.direction) &&
379                 this.precision.equals(other.precision);
380     }
381 
382     /** {@inheritDoc} */
383     @Override
384     public String toString() {
385         return MessageFormat.format(TO_STRING_FORMAT,
386                 getClass().getSimpleName(),
387                 getOrigin(),
388                 getDirection());
389     }
390 
391     /** Class containing a transformed line instance along with a subspace (1D) transform. The subspace
392      * transform produces the equivalent of the 3D transform in 1D.
393      */
394     public static final class SubspaceTransform {
395         /** The transformed line. */
396         private final Line3D line;
397 
398         /** The subspace transform instance. */
399         private final AffineTransformMatrix1D transform;
400 
401         /** Simple constructor.
402          * @param line the transformed line
403          * @param transform 1D transform that can be applied to subspace points
404          */
405         public SubspaceTransform(final Line3D line, final AffineTransformMatrix1D transform) {
406             this.line = line;
407             this.transform = transform;
408         }
409 
410         /** Get the transformed line instance.
411          * @return the transformed line instance
412          */
413         public Line3D getLine() {
414             return line;
415         }
416 
417         /** Get the 1D transform that can be applied to subspace points. This transform can be used
418          * to perform the equivalent of the 3D transform in 1D space.
419          * @return the subspace transform instance
420          */
421         public AffineTransformMatrix1D getTransform() {
422             return transform;
423         }
424     }
425 }