Line.java

  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.twod;

  18. import java.text.MessageFormat;
  19. import java.util.Objects;

  20. import org.apache.commons.geometry.core.Transform;
  21. import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
  22. import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
  23. import org.apache.commons.geometry.core.partitioning.Hyperplane;
  24. import org.apache.commons.geometry.euclidean.internal.Vectors;
  25. import org.apache.commons.geometry.euclidean.oned.AffineTransformMatrix1D;
  26. import org.apache.commons.geometry.euclidean.oned.Vector1D;
  27. import org.apache.commons.numbers.angle.Angle;
  28. import org.apache.commons.numbers.core.Precision;

  29. /** This class represents an oriented line in the 2D plane.

  30.  * <p>An oriented line can be defined either by extending a line
  31.  * segment between two points past these points, by specifying a
  32.  * point and a direction, or by specifying a point and an angle
  33.  * relative to the x-axis.</p>

  34.  * <p>Since the line oriented, the two half planes on its sides are
  35.  * unambiguously identified as the left half plane and the right half
  36.  * plane. This can be used to identify the interior and the exterior
  37.  * in a simple way when a line is used to define a portion of a polygon
  38.  * boundary.</p>

  39.  * <p>A line can also be used to completely define a reference frame
  40.  * in the plane. It is sufficient to select one specific point in the
  41.  * line (the orthogonal projection of the original reference frame on
  42.  * the line) and to use the unit vector in the line direction (see
  43.  * {@link #getDirection()} and the orthogonal vector oriented from the
  44.  * left half plane to the right half plane (see {@link #getOffsetDirection()}.
  45.  * We define two coordinates by the process, the <em>abscissa</em> along
  46.  * the line, and the <em>offset</em> across the line. All points of the
  47.  * plane are uniquely identified by these two coordinates. The line is
  48.  * the set of points at zero offset, the left half plane is the set of
  49.  * points with negative offsets and the right half plane is the set of
  50.  * points with positive offsets.</p>
  51.  * @see Lines
  52.  */
  53. public final class Line extends AbstractHyperplane<Vector2D>
  54.     implements EmbeddingHyperplane<Vector2D, Vector1D> {

  55.     /** Format string for creating line string representations. */
  56.     static final String TO_STRING_FORMAT = "{0}[origin= {1}, direction= {2}]";

  57.     /** The direction of the line as a normalized vector. */
  58.     private final Vector2D.Unit direction;

  59.     /** The distance between the origin and the line. */
  60.     private final double originOffset;

  61.     /** Simple constructor.
  62.      * @param direction The direction of the line.
  63.      * @param originOffset The signed distance between the line and the origin.
  64.      * @param precision Precision context used to compare floating point numbers.
  65.      */
  66.     Line(final Vector2D.Unit direction, final double originOffset, final Precision.DoubleEquivalence precision) {
  67.         super(precision);

  68.         this.direction = direction;
  69.         this.originOffset = originOffset;
  70.     }

  71.     /** Get the angle of the line in radians with respect to the abscissa (+x) axis. The
  72.      * returned angle is in the range {@code [0, 2pi)}.
  73.      * @return the angle of the line with respect to the abscissa (+x) axis in the range
  74.      *      {@code [0, 2pi)}
  75.      */
  76.     public double getAngle() {
  77.         final double angle = Math.atan2(direction.getY(), direction.getX());
  78.         return Angle.Rad.WITHIN_0_AND_2PI.applyAsDouble(angle);
  79.     }

  80.     /** Get the direction of the line.
  81.      * @return the direction of the line
  82.      */
  83.     public Vector2D.Unit getDirection() {
  84.         return direction;
  85.     }

  86.     /** Get the offset direction of the line. This vector is perpendicular to the
  87.      * line and points in the direction of positive offset values, meaning that
  88.      * it points from the left side of the line to the right when one is looking
  89.      * along the line direction.
  90.      * @return the offset direction of the line.
  91.      */
  92.     public Vector2D getOffsetDirection() {
  93.         return Vector2D.of(direction.getY(), -direction.getX());
  94.     }

  95.     /** Get the line origin point. This is the projection of the 2D origin
  96.      * onto the line and also serves as the origin for the 1D embedded subspace.
  97.      * @return the origin point of the line
  98.      */
  99.     public Vector2D getOrigin() {
  100.         return toSpace(Vector1D.ZERO);
  101.     }

  102.     /** Get the signed distance from the origin of the 2D space to the
  103.      * closest point on the line.
  104.      * @return the signed distance from the origin to the line
  105.      */
  106.     public double getOriginOffset() {
  107.         return originOffset;
  108.     }

  109.     /** {@inheritDoc} */
  110.     @Override
  111.     public Line reverse() {
  112.         return new Line(direction.negate(), -originOffset, getPrecision());
  113.     }

  114.     /** {@inheritDoc} */
  115.     @Override
  116.     public Line transform(final Transform<Vector2D> transform) {
  117.         final Vector2D origin = getOrigin();

  118.         final Vector2D tOrigin = transform.apply(origin);
  119.         final Vector2D tOriginPlusDir = transform.apply(origin.add(getDirection()));

  120.         return Lines.fromPoints(tOrigin, tOriginPlusDir, getPrecision());
  121.     }

  122.     /** Get an object containing the current line transformed by the argument along with a
  123.      * 1D transform that can be applied to subspace points. The subspace transform transforms
  124.      * subspace points such that their 2D location in the transformed line is the same as their
  125.      * 2D location in the original line after the 2D transform is applied. For example, consider
  126.      * the code below:
  127.      * <pre>
  128.      *      SubspaceTransform st = line.subspaceTransform(transform);
  129.      *
  130.      *      Vector1D subPt = Vector1D.of(1);
  131.      *
  132.      *      Vector2D a = transform.apply(line.toSpace(subPt)); // transform in 2D space
  133.      *      Vector2D b = st.getLine().toSpace(st.getTransform().apply(subPt)); // transform in 1D space
  134.      * </pre>
  135.      * At the end of execution, the points {@code a} (which was transformed using the original
  136.      * 2D transform) and {@code b} (which was transformed in 1D using the subspace transform)
  137.      * are equivalent.
  138.      *
  139.      * @param transform the transform to apply to this instance
  140.      * @return an object containing the transformed line along with a transform that can be applied
  141.      *      to subspace points
  142.      * @see #transform(Transform)
  143.      */
  144.     public SubspaceTransform subspaceTransform(final Transform<Vector2D> transform) {
  145.         final Vector2D origin = getOrigin();

  146.         final Vector2D p1 = transform.apply(origin);
  147.         final Vector2D p2 = transform.apply(origin.add(direction));

  148.         final Line tLine = Lines.fromPoints(p1, p2, getPrecision());

  149.         final Vector1D tSubspaceOrigin = tLine.toSubspace(p1);
  150.         final Vector1D tSubspaceDirection = tSubspaceOrigin.vectorTo(tLine.toSubspace(p2));

  151.         final double translation = tSubspaceOrigin.getX();
  152.         final double scale = tSubspaceDirection.getX();

  153.         final AffineTransformMatrix1D subspaceTransform = AffineTransformMatrix1D.of(scale, translation);

  154.         return new SubspaceTransform(tLine, subspaceTransform);
  155.     }

  156.     /** {@inheritDoc} */
  157.     @Override
  158.     public LineConvexSubset span() {
  159.         return Lines.span(this);
  160.     }

  161.     /** Create a new line segment from the given 1D interval. The returned line
  162.      * segment consists of all points between the two locations, regardless of the order the
  163.      * arguments are given.
  164.      * @param a first 1D location for the interval
  165.      * @param b second 1D location for the interval
  166.      * @return a new line segment on this line
  167.      * @throws IllegalArgumentException if either of the locations is NaN or infinite
  168.      * @see Lines#segmentFromLocations(Line, double, double)
  169.      */
  170.     public Segment segment(final double a, final double b) {
  171.         return Lines.segmentFromLocations(this, a, b);
  172.     }

  173.     /** Create a new line segment from two points. The returned segment represents all points on this line
  174.      * between the projected locations of {@code a} and {@code b}. The points may be given in any order.
  175.      * @param a first point
  176.      * @param b second point
  177.      * @return a new line segment on this line
  178.      * @throws IllegalArgumentException if either point contains NaN or infinite coordinate values
  179.      * @see Lines#segmentFromPoints(Line, Vector2D, Vector2D)
  180.      */
  181.     public Segment segment(final Vector2D a, final Vector2D b) {
  182.         return Lines.segmentFromPoints(this, a, b);
  183.     }

  184.     /** Create a new convex line subset that starts at infinity and continues along
  185.      * the line up to the projection of the given end point.
  186.      * @param endPoint point defining the end point of the line subset; the end point
  187.      *      is equal to the projection of this point onto the line
  188.      * @return a new, half-open line subset that ends at the given point
  189.      * @throws IllegalArgumentException if any coordinate in {@code endPoint} is NaN or infinite
  190.      * @see Lines#reverseRayFromPoint(Line, Vector2D)
  191.      */
  192.     public ReverseRay reverseRayTo(final Vector2D endPoint) {
  193.         return Lines.reverseRayFromPoint(this, endPoint);
  194.     }

  195.     /** Create a new convex line subset that starts at infinity and continues along
  196.      * the line up to the given 1D location.
  197.      * @param endLocation the 1D location of the end of the half-line
  198.      * @return a new, half-open line subset that ends at the given 1D location
  199.      * @throws IllegalArgumentException if {@code endLocation} is NaN or infinite
  200.      * @see Lines#reverseRayFromLocation(Line, double)
  201.      */
  202.     public ReverseRay reverseRayTo(final double endLocation) {
  203.         return Lines.reverseRayFromLocation(this, endLocation);
  204.     }

  205.     /** Create a new ray instance that starts at the projection of the given point
  206.      * and continues in the direction of the line to infinity.
  207.      * @param startPoint point defining the start point of the ray; the start point
  208.      *      is equal to the projection of this point onto the line
  209.      * @return a ray starting at the projected point and extending along this line
  210.      *      to infinity
  211.      * @throws IllegalArgumentException if any coordinate in {@code startPoint} is NaN or infinite
  212.      * @see Lines#rayFromPoint(Line, Vector2D)
  213.      */
  214.     public Ray rayFrom(final Vector2D startPoint) {
  215.         return Lines.rayFromPoint(this, startPoint);
  216.     }

  217.     /** Create a new ray instance that starts at the given 1D location and continues in
  218.      * the direction of the line to infinity.
  219.      * @param startLocation 1D location defining the start point of the ray
  220.      * @return a ray starting at the given 1D location and extending along this line
  221.      *      to infinity
  222.      * @throws IllegalArgumentException if {@code startLocation} is NaN or infinite
  223.      * @see Lines#rayFromLocation(Line, double)
  224.      */
  225.     public Ray rayFrom(final double startLocation) {
  226.         return Lines.rayFromLocation(this, startLocation);
  227.     }

  228.     /** Get the abscissa of the given point on the line. The abscissa represents
  229.      * the distance the projection of the point on the line is from the line's
  230.      * origin point (the point on the line closest to the origin of the
  231.      * 2D space). Abscissa values increase in the direction of the line. This method
  232.      * is exactly equivalent to {@link #toSubspace(Vector2D)} except that this method
  233.      * returns a double instead of a {@link Vector1D}.
  234.      * @param point point to compute the abscissa for
  235.      * @return abscissa value of the point
  236.      * @see #toSubspace(Vector2D)
  237.      */
  238.     public double abscissa(final Vector2D point) {
  239.         return direction.dot(point);
  240.     }

  241.     /** {@inheritDoc} */
  242.     @Override
  243.     public Vector1D toSubspace(final Vector2D point) {
  244.         return Vector1D.of(abscissa(point));
  245.     }

  246.     /** {@inheritDoc} */
  247.     @Override
  248.     public Vector2D toSpace(final Vector1D point) {
  249.         return toSpace(point.getX());
  250.     }

  251.     /** Convert the given abscissa value (1D location on the line)
  252.      * into a 2D point.
  253.      * @param abscissa value to convert
  254.      * @return 2D point corresponding to the line abscissa value
  255.      */
  256.     public Vector2D toSpace(final double abscissa) {
  257.         // The 2D coordinate is equal to the projection of the
  258.         // 2D origin onto the line plus the direction multiplied
  259.         // by the abscissa. We can combine everything into a single
  260.         // step below given that the origin location is equal to
  261.         // (-direction.y * originOffset, direction.x * originOffset).
  262.         return Vector2D.of(
  263.                     Vectors.linearCombination(abscissa, direction.getX(), -originOffset, direction.getY()),
  264.                     Vectors.linearCombination(abscissa, direction.getY(), originOffset, direction.getX())
  265.                 );
  266.     }

  267.     /** Get the intersection point of the instance and another line.
  268.      * @param other other line
  269.      * @return intersection point of the instance and the other line
  270.      *      or null if there is no unique intersection point (ie, the lines
  271.      *      are parallel or coincident)
  272.      */
  273.     public Vector2D intersection(final Line other) {
  274.         final double area = this.direction.signedArea(other.direction);
  275.         if (getPrecision().eqZero(area)) {
  276.             // lines are parallel
  277.             return null;
  278.         }

  279.         final double x = Vectors.linearCombination(
  280.                 other.direction.getX(), originOffset,
  281.                 -direction.getX(), other.originOffset) / area;

  282.         final double y = Vectors.linearCombination(
  283.                 other.direction.getY(), originOffset,
  284.                 -direction.getY(), other.originOffset) / area;

  285.         return Vector2D.of(x, y);
  286.     }

  287.     /** Compute the angle in radians between this instance's direction and the direction
  288.      * of the given line. The return value is in the range {@code [-pi, +pi)}. This method
  289.      * always returns a value, even for parallel or coincident lines.
  290.      * @param other other line
  291.      * @return the angle required to rotate this line to point in the direction of
  292.      *      the given line
  293.      */
  294.     public double angle(final Line other) {
  295.         final double thisAngle = Math.atan2(direction.getY(), direction.getX());
  296.         final double otherAngle = Math.atan2(other.direction.getY(), other.direction.getX());

  297.         return Angle.Rad.WITHIN_MINUS_PI_AND_PI.applyAsDouble(otherAngle - thisAngle);
  298.     }

  299.     /** {@inheritDoc} */
  300.     @Override
  301.     public Vector2D project(final Vector2D point) {
  302.         return toSpace(toSubspace(point));
  303.     }

  304.     /** {@inheritDoc} */
  305.     @Override
  306.     public double offset(final Vector2D point) {
  307.         return originOffset - direction.signedArea(point);
  308.     }

  309.     /** Get the offset (oriented distance) of the given line relative to this instance.
  310.      * Since an infinite number of distances can be calculated between points on two
  311.      * different lines, this method returns the value closest to zero. For intersecting
  312.      * lines, this will simply be zero. For parallel lines, this will be the
  313.      * perpendicular distance between the two lines, as a signed value.
  314.      *
  315.      * <p>The sign of the returned offset indicates the side of the line that the
  316.      * argument lies on. The offset is positive if the line lies on the right side
  317.      * of the instance and negative if the line lies on the left side
  318.      * of the instance.</p>
  319.      * @param line line to check
  320.      * @return offset of the line
  321.      * @see #distance(Line)
  322.      */
  323.     public double offset(final Line line) {
  324.         if (isParallel(line)) {
  325.             // since the lines are parallel, the offset between
  326.             // them is simply the difference between their origin offsets,
  327.             // with the second offset negated if the lines point if opposite
  328.             // directions
  329.             final double dot = direction.dot(line.direction);
  330.             return originOffset - (Math.signum(dot) * line.originOffset);
  331.         }

  332.         // the lines are not parallel, which means they intersect at some point
  333.         return 0.0;
  334.     }

  335.     /** {@inheritDoc} */
  336.     @Override
  337.     public boolean similarOrientation(final Hyperplane<Vector2D> other) {
  338.         final Line otherLine = (Line) other;
  339.         return direction.dot(otherLine.direction) >= 0.0;
  340.     }

  341.     /** Get one point from the plane, relative to the coordinate system
  342.      * of the line. Note that the direction of increasing offsets points
  343.      * to the <em>right</em> of the line. This means that if one pictures
  344.      * the line (abscissa) direction as equivalent to the +x-axis, the offset
  345.      * direction will point along the -y axis.
  346.      * @param abscissa desired abscissa (distance along the line) for the point
  347.      * @param offset desired offset (distance perpendicular to the line) for the point
  348.      * @return one point in the plane, with given abscissa and offset
  349.      *      relative to the line
  350.      */
  351.     public Vector2D pointAt(final double abscissa, final double offset) {
  352.         final double pointOffset = offset - originOffset;
  353.         return Vector2D.of(Vectors.linearCombination(abscissa, direction.getX(),  pointOffset, direction.getY()),
  354.                             Vectors.linearCombination(abscissa, direction.getY(), -pointOffset, direction.getX()));
  355.     }

  356.     /** Check if the line contains a point.
  357.      * @param p point to check
  358.      * @return true if p belongs to the line
  359.      */
  360.     @Override
  361.     public boolean contains(final Vector2D p) {
  362.         return getPrecision().eqZero(offset(p));
  363.     }

  364.     /** Check if this instance completely contains the other line.
  365.      * This will be true if the two instances represent the same line,
  366.      * with perhaps different directions.
  367.      * @param line line to check
  368.      * @return true if this instance contains all points in the given line
  369.      */
  370.     public boolean contains(final Line line) {
  371.         return isParallel(line) && getPrecision().eqZero(offset(line));
  372.     }

  373.     /** Compute the distance between the instance and a point.
  374.      * <p>This is a shortcut for invoking Math.abs(getOffset(p)),
  375.      * and provides consistency with what is in the
  376.      * org.apache.commons.geometry.euclidean.threed.Line class.</p>
  377.      *
  378.      * @param p to check
  379.      * @return distance between the instance and the point
  380.      */
  381.     public double distance(final Vector2D p) {
  382.         return Math.abs(offset(p));
  383.     }

  384.     /** Compute the shortest distance between this instance and
  385.      * the given line. This value will simply be zero for intersecting
  386.      * lines.
  387.      * @param line line to compute the closest distance to
  388.      * @return the shortest distance between this instance and the
  389.      *      given line
  390.      * @see #offset(Line)
  391.      */
  392.     public double distance(final Line line) {
  393.         return Math.abs(offset(line));
  394.     }

  395.     /** Check if the instance is parallel to another line.
  396.      * @param line other line to check
  397.      * @return true if the instance is parallel to the other line
  398.      *  (they can have either the same or opposite orientations)
  399.      */
  400.     public boolean isParallel(final Line line) {
  401.         final double area = direction.signedArea(line.direction);
  402.         return getPrecision().eqZero(area);
  403.     }

  404.     /** Return true if this instance should be considered equivalent to the argument, using the
  405.      * given precision context for comparison. Instances are considered equivalent if they have
  406.      * equivalent {@code origin} points and make similar angles with the x-axis.
  407.      * @param other the point to compare with
  408.      * @param precision precision context to use for the comparison
  409.      * @return true if this instance should be considered equivalent to the argument
  410.      * @see Vector2D#eq(Vector2D, Precision.DoubleEquivalence)
  411.      */
  412.     public boolean eq(final Line other, final Precision.DoubleEquivalence precision) {
  413.         return getOrigin().eq(other.getOrigin(), precision) &&
  414.                 precision.eq(getAngle(), other.getAngle());
  415.     }

  416.     /** {@inheritDoc} */
  417.     @Override
  418.     public int hashCode() {
  419.         final int prime = 167;

  420.         int result = 1;
  421.         result = (prime * result) + Objects.hashCode(direction);
  422.         result = (prime * result) + Double.hashCode(originOffset);
  423.         result = (prime * result) + Objects.hashCode(getPrecision());

  424.         return result;
  425.     }

  426.     /** {@inheritDoc} */
  427.     @Override
  428.     public boolean equals(final Object obj) {
  429.         if (this == obj) {
  430.             return true;
  431.         } else if (!(obj instanceof Line)) {
  432.             return false;
  433.         }

  434.         final Line other = (Line) obj;

  435.         return Objects.equals(this.direction, other.direction) &&
  436.                 Double.compare(this.originOffset, other.originOffset) == 0 &&
  437.                 Objects.equals(this.getPrecision(), other.getPrecision());
  438.     }

  439.     /** {@inheritDoc} */
  440.     @Override
  441.     public String toString() {
  442.         return MessageFormat.format(TO_STRING_FORMAT,
  443.                 getClass().getSimpleName(),
  444.                 getOrigin(),
  445.                 getDirection());
  446.     }

  447.     /** Class containing a transformed line instance along with a subspace (1D) transform. The subspace
  448.      * transform produces the equivalent of the 2D transform in 1D.
  449.      */
  450.     public static final class SubspaceTransform {
  451.         /** The transformed line. */
  452.         private final Line line;

  453.         /** The subspace transform instance. */
  454.         private final AffineTransformMatrix1D transform;

  455.         /** Simple constructor.
  456.          * @param line the transformed line
  457.          * @param transform 1D transform that can be applied to subspace points
  458.          */
  459.         public SubspaceTransform(final Line line, final AffineTransformMatrix1D transform) {
  460.             this.line = line;
  461.             this.transform = transform;
  462.         }

  463.         /** Get the transformed line instance.
  464.          * @return the transformed line instance
  465.          */
  466.         public Line getLine() {
  467.             return line;
  468.         }

  469.         /** Get the 1D transform that can be applied to subspace points. This transform can be used
  470.          * to perform the equivalent of the 2D transform in 1D space.
  471.          * @return the subspace transform instance
  472.          */
  473.         public AffineTransformMatrix1D getTransform() {
  474.             return transform;
  475.         }
  476.     }
  477. }