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.twod;
018
019import java.text.MessageFormat;
020import java.util.Objects;
021
022import org.apache.commons.geometry.core.Transform;
023import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
024import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
025import org.apache.commons.geometry.core.partitioning.Hyperplane;
026import org.apache.commons.geometry.euclidean.internal.Vectors;
027import org.apache.commons.geometry.euclidean.oned.AffineTransformMatrix1D;
028import org.apache.commons.geometry.euclidean.oned.Vector1D;
029import org.apache.commons.numbers.angle.Angle;
030import org.apache.commons.numbers.core.Precision;
031
032/** This class represents an oriented line in the 2D plane.
033
034 * <p>An oriented line can be defined either by extending a line
035 * segment between two points past these points, by specifying a
036 * point and a direction, or by specifying a point and an angle
037 * relative to the x-axis.</p>
038
039 * <p>Since the line oriented, the two half planes on its sides are
040 * unambiguously identified as the left half plane and the right half
041 * plane. This can be used to identify the interior and the exterior
042 * in a simple way when a line is used to define a portion of a polygon
043 * boundary.</p>
044
045 * <p>A line can also be used to completely define a reference frame
046 * in the plane. It is sufficient to select one specific point in the
047 * line (the orthogonal projection of the original reference frame on
048 * the line) and to use the unit vector in the line direction (see
049 * {@link #getDirection()} and the orthogonal vector oriented from the
050 * left half plane to the right half plane (see {@link #getOffsetDirection()}.
051 * We define two coordinates by the process, the <em>abscissa</em> along
052 * the line, and the <em>offset</em> across the line. All points of the
053 * plane are uniquely identified by these two coordinates. The line is
054 * the set of points at zero offset, the left half plane is the set of
055 * points with negative offsets and the right half plane is the set of
056 * points with positive offsets.</p>
057 * @see Lines
058 */
059public final class Line extends AbstractHyperplane<Vector2D>
060    implements EmbeddingHyperplane<Vector2D, Vector1D> {
061
062    /** Format string for creating line string representations. */
063    static final String TO_STRING_FORMAT = "{0}[origin= {1}, direction= {2}]";
064
065    /** The direction of the line as a normalized vector. */
066    private final Vector2D.Unit direction;
067
068    /** The distance between the origin and the line. */
069    private final double originOffset;
070
071    /** Simple constructor.
072     * @param direction The direction of the line.
073     * @param originOffset The signed distance between the line and the origin.
074     * @param precision Precision context used to compare floating point numbers.
075     */
076    Line(final Vector2D.Unit direction, final double originOffset, final Precision.DoubleEquivalence precision) {
077        super(precision);
078
079        this.direction = direction;
080        this.originOffset = originOffset;
081    }
082
083    /** Get the angle of the line in radians with respect to the abscissa (+x) axis. The
084     * returned angle is in the range {@code [0, 2pi)}.
085     * @return the angle of the line with respect to the abscissa (+x) axis in the range
086     *      {@code [0, 2pi)}
087     */
088    public double getAngle() {
089        final double angle = Math.atan2(direction.getY(), direction.getX());
090        return Angle.Rad.WITHIN_0_AND_2PI.applyAsDouble(angle);
091    }
092
093    /** Get the direction of the line.
094     * @return the direction of the line
095     */
096    public Vector2D.Unit getDirection() {
097        return direction;
098    }
099
100    /** Get the offset direction of the line. This vector is perpendicular to the
101     * line and points in the direction of positive offset values, meaning that
102     * it points from the left side of the line to the right when one is looking
103     * along the line direction.
104     * @return the offset direction of the line.
105     */
106    public Vector2D getOffsetDirection() {
107        return Vector2D.of(direction.getY(), -direction.getX());
108    }
109
110    /** Get the line origin point. This is the projection of the 2D origin
111     * onto the line and also serves as the origin for the 1D embedded subspace.
112     * @return the origin point of the line
113     */
114    public Vector2D getOrigin() {
115        return toSpace(Vector1D.ZERO);
116    }
117
118    /** Get the signed distance from the origin of the 2D space to the
119     * closest point on the line.
120     * @return the signed distance from the origin to the line
121     */
122    public double getOriginOffset() {
123        return originOffset;
124    }
125
126    /** {@inheritDoc} */
127    @Override
128    public Line reverse() {
129        return new Line(direction.negate(), -originOffset, getPrecision());
130    }
131
132    /** {@inheritDoc} */
133    @Override
134    public Line transform(final Transform<Vector2D> transform) {
135        final Vector2D origin = getOrigin();
136
137        final Vector2D tOrigin = transform.apply(origin);
138        final Vector2D tOriginPlusDir = transform.apply(origin.add(getDirection()));
139
140        return Lines.fromPoints(tOrigin, tOriginPlusDir, getPrecision());
141    }
142
143    /** Get an object containing the current line transformed by the argument along with a
144     * 1D transform that can be applied to subspace points. The subspace transform transforms
145     * subspace points such that their 2D location in the transformed line is the same as their
146     * 2D location in the original line after the 2D transform is applied. For example, consider
147     * the code below:
148     * <pre>
149     *      SubspaceTransform st = line.subspaceTransform(transform);
150     *
151     *      Vector1D subPt = Vector1D.of(1);
152     *
153     *      Vector2D a = transform.apply(line.toSpace(subPt)); // transform in 2D space
154     *      Vector2D b = st.getLine().toSpace(st.getTransform().apply(subPt)); // transform in 1D space
155     * </pre>
156     * At the end of execution, the points {@code a} (which was transformed using the original
157     * 2D transform) and {@code b} (which was transformed in 1D using the subspace transform)
158     * are equivalent.
159     *
160     * @param transform the transform to apply to this instance
161     * @return an object containing the transformed line along with a transform that can be applied
162     *      to subspace points
163     * @see #transform(Transform)
164     */
165    public SubspaceTransform subspaceTransform(final Transform<Vector2D> transform) {
166        final Vector2D origin = getOrigin();
167
168        final Vector2D p1 = transform.apply(origin);
169        final Vector2D p2 = transform.apply(origin.add(direction));
170
171        final Line tLine = Lines.fromPoints(p1, p2, getPrecision());
172
173        final Vector1D tSubspaceOrigin = tLine.toSubspace(p1);
174        final Vector1D tSubspaceDirection = tSubspaceOrigin.vectorTo(tLine.toSubspace(p2));
175
176        final double translation = tSubspaceOrigin.getX();
177        final double scale = tSubspaceDirection.getX();
178
179        final AffineTransformMatrix1D subspaceTransform = AffineTransformMatrix1D.of(scale, translation);
180
181        return new SubspaceTransform(tLine, subspaceTransform);
182    }
183
184    /** {@inheritDoc} */
185    @Override
186    public LineConvexSubset span() {
187        return Lines.span(this);
188    }
189
190    /** Create a new line segment from the given 1D interval. The returned line
191     * segment consists of all points between the two locations, regardless of the order the
192     * arguments are given.
193     * @param a first 1D location for the interval
194     * @param b second 1D location for the interval
195     * @return a new line segment on this line
196     * @throws IllegalArgumentException if either of the locations is NaN or infinite
197     * @see Lines#segmentFromLocations(Line, double, double)
198     */
199    public Segment segment(final double a, final double b) {
200        return Lines.segmentFromLocations(this, a, b);
201    }
202
203    /** Create a new line segment from two points. The returned segment represents all points on this line
204     * between the projected locations of {@code a} and {@code b}. The points may be given in any order.
205     * @param a first point
206     * @param b second point
207     * @return a new line segment on this line
208     * @throws IllegalArgumentException if either point contains NaN or infinite coordinate values
209     * @see Lines#segmentFromPoints(Line, Vector2D, Vector2D)
210     */
211    public Segment segment(final Vector2D a, final Vector2D b) {
212        return Lines.segmentFromPoints(this, a, b);
213    }
214
215    /** Create a new convex line subset that starts at infinity and continues along
216     * the line up to the projection of the given end point.
217     * @param endPoint point defining the end point of the line subset; the end point
218     *      is equal to the projection of this point onto the line
219     * @return a new, half-open line subset that ends at the given point
220     * @throws IllegalArgumentException if any coordinate in {@code endPoint} is NaN or infinite
221     * @see Lines#reverseRayFromPoint(Line, Vector2D)
222     */
223    public ReverseRay reverseRayTo(final Vector2D endPoint) {
224        return Lines.reverseRayFromPoint(this, endPoint);
225    }
226
227    /** Create a new convex line subset that starts at infinity and continues along
228     * the line up to the given 1D location.
229     * @param endLocation the 1D location of the end of the half-line
230     * @return a new, half-open line subset that ends at the given 1D location
231     * @throws IllegalArgumentException if {@code endLocation} is NaN or infinite
232     * @see Lines#reverseRayFromLocation(Line, double)
233     */
234    public ReverseRay reverseRayTo(final double endLocation) {
235        return Lines.reverseRayFromLocation(this, endLocation);
236    }
237
238    /** Create a new ray instance that starts at the projection of the given point
239     * and continues in the direction of the line to infinity.
240     * @param startPoint point defining the start point of the ray; the start point
241     *      is equal to the projection of this point onto the line
242     * @return a ray starting at the projected point and extending along this line
243     *      to infinity
244     * @throws IllegalArgumentException if any coordinate in {@code startPoint} is NaN or infinite
245     * @see Lines#rayFromPoint(Line, Vector2D)
246     */
247    public Ray rayFrom(final Vector2D startPoint) {
248        return Lines.rayFromPoint(this, startPoint);
249    }
250
251    /** Create a new ray instance that starts at the given 1D location and continues in
252     * the direction of the line to infinity.
253     * @param startLocation 1D location defining the start point of the ray
254     * @return a ray starting at the given 1D location and extending along this line
255     *      to infinity
256     * @throws IllegalArgumentException if {@code startLocation} is NaN or infinite
257     * @see Lines#rayFromLocation(Line, double)
258     */
259    public Ray rayFrom(final double startLocation) {
260        return Lines.rayFromLocation(this, startLocation);
261    }
262
263    /** Get the abscissa of the given point on the line. The abscissa represents
264     * the distance the projection of the point on the line is from the line's
265     * origin point (the point on the line closest to the origin of the
266     * 2D space). Abscissa values increase in the direction of the line. This method
267     * is exactly equivalent to {@link #toSubspace(Vector2D)} except that this method
268     * returns a double instead of a {@link Vector1D}.
269     * @param point point to compute the abscissa for
270     * @return abscissa value of the point
271     * @see #toSubspace(Vector2D)
272     */
273    public double abscissa(final Vector2D point) {
274        return direction.dot(point);
275    }
276
277    /** {@inheritDoc} */
278    @Override
279    public Vector1D toSubspace(final Vector2D point) {
280        return Vector1D.of(abscissa(point));
281    }
282
283    /** {@inheritDoc} */
284    @Override
285    public Vector2D toSpace(final Vector1D point) {
286        return toSpace(point.getX());
287    }
288
289    /** Convert the given abscissa value (1D location on the line)
290     * into a 2D point.
291     * @param abscissa value to convert
292     * @return 2D point corresponding to the line abscissa value
293     */
294    public Vector2D toSpace(final double abscissa) {
295        // The 2D coordinate is equal to the projection of the
296        // 2D origin onto the line plus the direction multiplied
297        // by the abscissa. We can combine everything into a single
298        // step below given that the origin location is equal to
299        // (-direction.y * originOffset, direction.x * originOffset).
300        return Vector2D.of(
301                    Vectors.linearCombination(abscissa, direction.getX(), -originOffset, direction.getY()),
302                    Vectors.linearCombination(abscissa, direction.getY(), originOffset, direction.getX())
303                );
304    }
305
306    /** Get the intersection point of the instance and another line.
307     * @param other other line
308     * @return intersection point of the instance and the other line
309     *      or null if there is no unique intersection point (ie, the lines
310     *      are parallel or coincident)
311     */
312    public Vector2D intersection(final Line other) {
313        final double area = this.direction.signedArea(other.direction);
314        if (getPrecision().eqZero(area)) {
315            // lines are parallel
316            return null;
317        }
318
319        final double x = Vectors.linearCombination(
320                other.direction.getX(), originOffset,
321                -direction.getX(), other.originOffset) / area;
322
323        final double y = Vectors.linearCombination(
324                other.direction.getY(), originOffset,
325                -direction.getY(), other.originOffset) / area;
326
327        return Vector2D.of(x, y);
328    }
329
330    /** Compute the angle in radians between this instance's direction and the direction
331     * of the given line. The return value is in the range {@code [-pi, +pi)}. This method
332     * always returns a value, even for parallel or coincident lines.
333     * @param other other line
334     * @return the angle required to rotate this line to point in the direction of
335     *      the given line
336     */
337    public double angle(final Line other) {
338        final double thisAngle = Math.atan2(direction.getY(), direction.getX());
339        final double otherAngle = Math.atan2(other.direction.getY(), other.direction.getX());
340
341        return Angle.Rad.WITHIN_MINUS_PI_AND_PI.applyAsDouble(otherAngle - thisAngle);
342    }
343
344    /** {@inheritDoc} */
345    @Override
346    public Vector2D project(final Vector2D point) {
347        return toSpace(toSubspace(point));
348    }
349
350    /** {@inheritDoc} */
351    @Override
352    public double offset(final Vector2D point) {
353        return originOffset - direction.signedArea(point);
354    }
355
356    /** Get the offset (oriented distance) of the given line relative to this instance.
357     * Since an infinite number of distances can be calculated between points on two
358     * different lines, this method returns the value closest to zero. For intersecting
359     * lines, this will simply be zero. For parallel lines, this will be the
360     * perpendicular distance between the two lines, as a signed value.
361     *
362     * <p>The sign of the returned offset indicates the side of the line that the
363     * argument lies on. The offset is positive if the line lies on the right side
364     * of the instance and negative if the line lies on the left side
365     * of the instance.</p>
366     * @param line line to check
367     * @return offset of the line
368     * @see #distance(Line)
369     */
370    public double offset(final Line line) {
371        if (isParallel(line)) {
372            // since the lines are parallel, the offset between
373            // them is simply the difference between their origin offsets,
374            // with the second offset negated if the lines point if opposite
375            // directions
376            final double dot = direction.dot(line.direction);
377            return originOffset - (Math.signum(dot) * line.originOffset);
378        }
379
380        // the lines are not parallel, which means they intersect at some point
381        return 0.0;
382    }
383
384    /** {@inheritDoc} */
385    @Override
386    public boolean similarOrientation(final Hyperplane<Vector2D> other) {
387        final Line otherLine = (Line) other;
388        return direction.dot(otherLine.direction) >= 0.0;
389    }
390
391    /** Get one point from the plane, relative to the coordinate system
392     * of the line. Note that the direction of increasing offsets points
393     * to the <em>right</em> of the line. This means that if one pictures
394     * the line (abscissa) direction as equivalent to the +x-axis, the offset
395     * direction will point along the -y axis.
396     * @param abscissa desired abscissa (distance along the line) for the point
397     * @param offset desired offset (distance perpendicular to the line) for the point
398     * @return one point in the plane, with given abscissa and offset
399     *      relative to the line
400     */
401    public Vector2D pointAt(final double abscissa, final double offset) {
402        final double pointOffset = offset - originOffset;
403        return Vector2D.of(Vectors.linearCombination(abscissa, direction.getX(),  pointOffset, direction.getY()),
404                            Vectors.linearCombination(abscissa, direction.getY(), -pointOffset, direction.getX()));
405    }
406
407    /** Check if the line contains a point.
408     * @param p point to check
409     * @return true if p belongs to the line
410     */
411    @Override
412    public boolean contains(final Vector2D p) {
413        return getPrecision().eqZero(offset(p));
414    }
415
416    /** Check if this instance completely contains the other line.
417     * This will be true if the two instances represent the same line,
418     * with perhaps different directions.
419     * @param line line to check
420     * @return true if this instance contains all points in the given line
421     */
422    public boolean contains(final Line line) {
423        return isParallel(line) && getPrecision().eqZero(offset(line));
424    }
425
426    /** Compute the distance between the instance and a point.
427     * <p>This is a shortcut for invoking Math.abs(getOffset(p)),
428     * and provides consistency with what is in the
429     * org.apache.commons.geometry.euclidean.threed.Line class.</p>
430     *
431     * @param p to check
432     * @return distance between the instance and the point
433     */
434    public double distance(final Vector2D p) {
435        return Math.abs(offset(p));
436    }
437
438    /** Compute the shortest distance between this instance and
439     * the given line. This value will simply be zero for intersecting
440     * lines.
441     * @param line line to compute the closest distance to
442     * @return the shortest distance between this instance and the
443     *      given line
444     * @see #offset(Line)
445     */
446    public double distance(final Line line) {
447        return Math.abs(offset(line));
448    }
449
450    /** Check if the instance is parallel to another line.
451     * @param line other line to check
452     * @return true if the instance is parallel to the other line
453     *  (they can have either the same or opposite orientations)
454     */
455    public boolean isParallel(final Line line) {
456        final double area = direction.signedArea(line.direction);
457        return getPrecision().eqZero(area);
458    }
459
460    /** Return true if this instance should be considered equivalent to the argument, using the
461     * given precision context for comparison. Instances are considered equivalent if they have
462     * equivalent {@code origin} points and make similar angles with the x-axis.
463     * @param other the point to compare with
464     * @param precision precision context to use for the comparison
465     * @return true if this instance should be considered equivalent to the argument
466     * @see Vector2D#eq(Vector2D, Precision.DoubleEquivalence)
467     */
468    public boolean eq(final Line other, final Precision.DoubleEquivalence precision) {
469        return getOrigin().eq(other.getOrigin(), precision) &&
470                precision.eq(getAngle(), other.getAngle());
471    }
472
473    /** {@inheritDoc} */
474    @Override
475    public int hashCode() {
476        final int prime = 167;
477
478        int result = 1;
479        result = (prime * result) + Objects.hashCode(direction);
480        result = (prime * result) + Double.hashCode(originOffset);
481        result = (prime * result) + Objects.hashCode(getPrecision());
482
483        return result;
484    }
485
486    /** {@inheritDoc} */
487    @Override
488    public boolean equals(final Object obj) {
489        if (this == obj) {
490            return true;
491        } else if (!(obj instanceof Line)) {
492            return false;
493        }
494
495        final Line other = (Line) obj;
496
497        return Objects.equals(this.direction, other.direction) &&
498                Double.compare(this.originOffset, other.originOffset) == 0 &&
499                Objects.equals(this.getPrecision(), other.getPrecision());
500    }
501
502    /** {@inheritDoc} */
503    @Override
504    public String toString() {
505        return MessageFormat.format(TO_STRING_FORMAT,
506                getClass().getSimpleName(),
507                getOrigin(),
508                getDirection());
509    }
510
511    /** Class containing a transformed line instance along with a subspace (1D) transform. The subspace
512     * transform produces the equivalent of the 2D transform in 1D.
513     */
514    public static final class SubspaceTransform {
515        /** The transformed line. */
516        private final Line line;
517
518        /** The subspace transform instance. */
519        private final AffineTransformMatrix1D transform;
520
521        /** Simple constructor.
522         * @param line the transformed line
523         * @param transform 1D transform that can be applied to subspace points
524         */
525        public SubspaceTransform(final Line line, final AffineTransformMatrix1D transform) {
526            this.line = line;
527            this.transform = transform;
528        }
529
530        /** Get the transformed line instance.
531         * @return the transformed line instance
532         */
533        public Line getLine() {
534            return line;
535        }
536
537        /** Get the 1D transform that can be applied to subspace points. This transform can be used
538         * to perform the equivalent of the 2D transform in 1D space.
539         * @return the subspace transform instance
540         */
541        public AffineTransformMatrix1D getTransform() {
542            return transform;
543        }
544    }
545}