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.euclidean.oned.AffineTransformMatrix1D;
25 import org.apache.commons.geometry.euclidean.oned.Vector1D;
26 import org.apache.commons.geometry.euclidean.threed.Vector3D;
27 import org.apache.commons.numbers.core.Precision;
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 Precision.DoubleEquivalence 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 Precision.DoubleEquivalence 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 Precision.DoubleEquivalence 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.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 }