/******************************************************************************* * Copyright (c) 2010, 2016 Research Group Software Construction, * RWTH Aachen University and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Alexander Nyßen (Research Group Software Contruction, RWTH Aachen University) - initial API and implementation * Matthias Wienand (itemis AG) - contribution for Bugzilla #355997 * *******************************************************************************/ package org.eclipse.gef.geometry.euclidean; import java.io.Serializable; import org.eclipse.gef.geometry.internal.utils.PrecisionUtils; import org.eclipse.gef.geometry.planar.Line; import org.eclipse.gef.geometry.planar.Point; import org.eclipse.gef.geometry.projective.Straight3D; import org.eclipse.gef.geometry.projective.Vector3D; /** * Represents a straight line within 2-dimensional Euclidean space. * * @author anyssen * @author mwienand * */ public class Straight implements Cloneable, Serializable { private static final long serialVersionUID = 1L; /** * <p> * Computes the counter-clockwise (CCW) signed distance of the third * {@link Point} to the {@link Straight} through the first two {@link Point} * s. * </p> * <p> * The CCW signed distance is positive if the three {@link Point}s are in * counter-clockwise order and negative if the {@link Point}s are in * clockwise order. It is zero if the third {@link Point} lies on the line. * </p> * <p> * If the first two {@link Point}s are equal to each other, this method * returns the distance of the first {@link Point} to the last {@link Point} * . * </p> * * @param p * the start {@link Point} of the {@link Straight} * @param q * the end {@link Point} of the {@link Straight} * @param r * the relative {@link Point} * @return the CCW signed distance of the {@link Point} r to the * {@link Straight} through {@link Point}s p and q */ public static double getSignedDistanceCCW(Point p, Point q, Point r) { Straight3D line = Straight3D.through(new Vector3D(p), new Vector3D(q)); if (line == null) { return 0d; } return -line.getSignedDistanceCW(new Vector3D(r)); } /** The position {@link Vector} of this {@link Straight}. */ public Vector position; /** The direction {@link Vector} of this {@link Straight}. */ public Vector direction; /** * Constructs a new {@link Straight} through the start and end {@link Point} * of the given {@link Line}. * * @param line * the {@link Line} which the new {@link Straight} shall pass * through */ public Straight(Line line) { this(line.getP1(), line.getP2()); } /** * Constructs a new {@link Straight} that passes through the two given * {@link Point}s. * * @param point1 * a first waypoint of the {@link Straight} to be constructed * @param point2 * a second waypoint of the {@link Straight} to be constructed */ public Straight(Point point1, Point point2) { this(new Vector(point1), new Vector(point1, point2)); } /** * Constructs a new {@link Straight} with the given position {@link Vector} * and direction {@link Vector}. * * @param position * a support {@link Vector} of the new {@link Straight} * @param direction * a direction {@link Vector} of the new {@link Straight} */ public Straight(Vector position, Vector direction) { this.position = position.clone(); this.direction = direction.clone(); } @Override public Straight clone() { return getCopy(); } /** * Checks if the {@link Point} indicated by the provided {@link Vector} is a * {@link Point} on this {@link Straight}. * * @param vector * the {@link Vector} that is checked to lie on this * {@link Straight} * @return <code>true</code> if the {@link Point} indicated by the given * {@link Vector} is a {@link Point} of this {@link Straight}, * otherwise <code>false</code> */ public boolean contains(Vector vector) { // deal with rounding effects here return PrecisionUtils.equal(getDistance(vector), 0); } /** * Checks if the {@link Point} indicated by the provided {@link Vector} is a * {@link Point} on the {@link Straight} segment between the given start and * end {@link Point}s indicated by their corresponding position * {@link Vector}s. * * @param segmentStart * A {@link Vector} indicating the start {@link Point} of the * segment. It has to lie on this {@link Straight}. * @param segmentEnd * A {@link Vector} indicating the end {@link Point} of the * segment. It has to lie on this {@link Straight}. * @param vector * The {@link Vector} that is checked for containment. * @return <code>true</code> if the {@link Point} indicated by the given * {@link Vector} lies on this {@link Straight}, within the * specified segment, otherwise <code>false</code> */ public boolean containsWithinSegment(Vector segmentStart, Vector segmentEnd, Vector vector) { // precondition: segment start and end have to be points on this // straight. if (!contains(segmentStart) || !contains(segmentEnd)) { throw new IllegalArgumentException( "segment points have to be contained"); //$NON-NLS-1$ } // check if segmentStart->segmentEnd is a legal segment or a single // point Vector segmentDirection = segmentEnd.getSubtracted(segmentStart); if (segmentDirection.isNull()) { return segmentStart.equals(vector); } // legal segment if (new Straight(segmentStart, segmentDirection).contains(vector)) { // compute parameter s, so that vector = segmentStart + s * // (segmentEnd - segmentStart). double s = segmentDirection.y != 0 ? (vector.y - segmentStart.y) / segmentDirection.y : (vector.x - segmentStart.x) / segmentDirection.x; // if s is between 0 and 1, the intersection point lies within // segment if (PrecisionUtils.smallerEqual(0, s) && PrecisionUtils.smallerEqual(s, 1)) { return true; } } return false; } /** * Checks if this {@link Straight} is equal to the provided {@link Straight} * . Two {@link Straight}s s1 and s2 are equal, if the position * {@link Vector} of s2 is a {@link Point} on s1 and the direction * {@link Vector}s of s1 and s2 are parallel. * * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object other) { if (!(other instanceof Straight)) { return false; } else { Straight otherStraight = (Straight) other; return contains(otherStraight.position) && isParallelTo(otherStraight); } } /** * Returns the (smallest) {@link Angle} between this {@link Straight} and * the provided one. * * @param other * the {@link Straight} to compute the {@link Angle} with * @return the {@link Angle} spanned between the two {@link Straight}s */ public Angle getAngle(Straight other) { return direction.getAngle(other.direction); } /** * <p> * Returns the counter-clockwise (CCW) or positive {@link Angle} spanned * between the two {@link Straight}s. * </p> * <p> * The returned {@link Angle} is the semi-opposite (see * {@link Angle#getOppositeSemi()}) of the {@link Angle} returned by the * {@link Straight#getAngleCW(Straight)} method except that for 180deg/0deg, * both methods return an {@link Angle} of 0deg. * </p> * * @param other * The {@link Straight} to which the {@link Angle} is computed. * @return the counter-clockwise (CCW) or positive {@link Angle} spanned * between the two {@link Straight}s */ public Angle getAngleCCW(Straight other) { Angle angle = getAngle(other); if (direction.getCrossProduct(other.direction) > 0) { angle = angle.getOppositeSemi(); } return angle; } /** * <p> * Returns the clockwise (CW) or negative {@link Angle} spanned between the * two {@link Straight}s. * </p> * <p> * The returned {@link Angle} is the semi-opposite (see * {@link Angle#getOppositeSemi()}) of the {@link Angle} returned by the * {@link Straight#getAngleCCW(Straight)} method except that for * 180deg/0deg, both methods return an {@link Angle} of 0deg. * </p> * * @param other * The {@link Straight} to which the {@link Angle} is computed. * @return the clockwise (CW) or negative {@link Angle} spanned between the * two {@link Straight}s */ public Angle getAngleCW(Straight other) { Angle angle = getAngleCCW(other); Angle angle0 = Angle.fromRad(0); if (angle.equals(angle0)) { // special-case 0deg: CW/CCW does not matter return angle0; } return angle.getOppositeSemi(); } /** * Returns a copy of this {@link Straight} object. * * @return a copy of this {@link Straight} object. */ public Straight getCopy() { return new Straight(position, direction); } /** * Returns the distance of the provided {@link Vector} to this * {@link Straight}, which is the distance between the provided * {@link Vector} and its projection onto this {@link Straight} (see * {@link Straight#getProjection(Vector)}). * * @param vector * the {@link Vector} whose distance to this {@link Straight} is * to be calculated * @return the distance between this {@link Straight} and the provided * {@link Vector} */ public double getDistance(Vector vector) { return getProjection(vector).getSubtracted(vector).getLength(); } /** * Computes and returns the position {@link Vector} of the intersection of * this {@link Straight} and the provided one. If the two {@link Straight}s * are parallel or identical, <code>null</code> is returned. * * @param other * the {@link Straight} to compute the position {@link Vector} of * the intersection with * @return a {@link Vector} pointing to the intersection point, * <code>null</code> if no intersection {@link Point} exists */ public Vector getIntersection(Straight other) { Vector p1 = this.position.getAdded(this.direction); Vector p2 = other.position.getAdded(other.direction); Vector3D l1 = new Vector3D(this.position.x, this.position.y, 1) .getCrossProduct(new Vector3D(p1.toPoint())); Vector3D l2 = new Vector3D(other.position.x, other.position.y, 1) .getCrossProduct(new Vector3D(p2.toPoint())); Point poi = l1.getCrossProduct(l2).toPoint(); return poi == null ? null : new Vector(poi); } /** * <p> * Returns this {@link Straight}'s parameter value for the given * {@link Vector}. If the given {@link Vector} is not on this * {@link Straight} an {@link IllegalArgumentException} is thrown. * </p> * <p> * This method is the reverse of the * {@link Straight#getPositionVectorAt(double)} method. * </p> * * @param vp * a {@link Vector} on this {@link Straight} for which the * parameter value is to be calculated * @return this {@link Straight}'s parameter value for the given * {@link Vector} */ public double getParameterAt(Vector vp) { if (!contains(vp)) { throw new IllegalArgumentException( "The given position Vector has to be on this Straight: getParameterAt(" + vp + "), this = " + this); } if (Math.abs(direction.x) > Math.abs(direction.y)) { return (vp.x - position.x) / direction.x; } if (direction.y != 0) { return (vp.y - position.y) / direction.y; } throw new IllegalStateException( "The direction Vector of this Straight may not be (0, 0) for this computation: getParameterAt(" + vp + "), this = " + this); } /** * <p> * Returns the {@link Vector} on this {@link Straight} at the given * parameter value. The {@link Vector} that you get is calculated by * multiplying this {@link Straight}'s direction {@link Vector} by the * parameter value and translating that {@link Vector} by this * {@link Straight}'s position {@link Vector}. * </p> * <p> * This method is the reverse of the {@link Straight#getParameterAt(Vector)} * method. * </p> * * @param parameter * the parameter value for which the corresponding {@link Vector} * on this {@link Straight} is to be calculated * @return the {@link Vector} on this {@link Straight} at the passed-in * parameter value */ public Vector getPositionVectorAt(double parameter) { return new Vector(position.x + direction.x * parameter, position.y + direction.y * parameter); } /** * Returns the projection of the given {@link Vector} onto this * {@link Straight}, which is the {@link Point} on this {@link Straight} * with the minimal distance to the {@link Point}, denoted by the provided * {@link Vector}. * * @param vector * the {@link Vector} whose projection should be determined * @return a new {@link Vector} representing the projection of the provided * {@link Vector} onto this {@link Straight} */ public Vector getProjection(Vector vector) { // calculate with a normalized direction vector to prevent rounding // effects Vector normalized = direction.getNormalized(); // to compensate rounding problems with large vectors, we shift // straight and given vector by the straight's position vector before // the computation and back before returning the computed projection. Straight s1 = new Straight(Vector.NULL, normalized); Straight s2 = new Straight(vector.getSubtracted(position), normalized.getOrthogonalComplement()); return s1.getIntersection(s2).getAdded(position); } /** * <p> * Returns the counter-clockwise (CCW) signed distance of the given * {@link Vector} to this {@link Straight}. * </p> * <p> * The CCW signed distance indicates on which side of the {@link Straight} * the {@link Vector} lies. If it lies on the right side of this * {@link Straight}'s direction {@link Vector}, the CCW signed distance is * negative. If it is on the left side of this {@link Straight}'s direction * {@link Vector}, it is positive. * </p> * * @param vector * the {@link Vector} for which the CCW signed distance to this * {@link Straight} is to be calculated * @return the CCW signed distance of the given {@link Vector} to this * {@link Straight} */ public double getSignedDistanceCCW(Vector vector) { return Straight.getSignedDistanceCCW(this.position.toPoint(), this.position.getAdded(this.direction).toPoint(), vector.toPoint()); } /** * <p> * Returns the clockwise (CW) signed distance of the given {@link Vector} to * this {@link Straight}. * </p> * <p> * The CW signed distance indicates on which side of the {@link Straight} * the {@link Vector} lies. If it is on the right side of this * {@link Straight}'s direction {@link Vector}, the CW signed distance is * positive. If it is on the left side of this {@link Straight}'s direction * {@link Vector}, it is negative. * </p> * * @param vector * the {@link Vector} for which the CW signed distance to this * {@link Straight} is to be calculated * @return the CW signed distance of the given {@link Vector} to this * {@link Straight} */ public double getSignedDistanceCW(Vector vector) { return -getSignedDistanceCCW(vector); } /** * @see java.lang.Object#hashCode() */ @Override public int hashCode() { // calculating a better hashCode is not possible, because due to the // imprecision, equals() is no longer transitive return 0; } /** * Checks whether this {@link Straight} and the provided one have a single * {@link Point} of intersection. * * @param other * the {@link Straight} to test for an intersection {@link Point} * with this {@link Straight} * @return <code>true</code> if the two {@link Straight}s intersect in one * single {@link Point}, otherwise <code>false</code> */ public boolean intersects(Straight other) { return !PrecisionUtils.equal(direction.getDotProduct( other.direction.getOrthogonalComplement()), 0, +6); } /** * Checks whether this {@link Straight} and the provided one have an * intersection {@link Point} which is inside the specified segment between * the segmentStart and segmentEnd {@link Vector}s. * * @param segmentStart * A {@link Vector} indicating the start {@link Point} of the * segment. It has to be a {@link Point} on the {@link Straight}. * @param segmentEnd * A {@link Vector} indicating the end {@link Point} of the * segment. It has to be a {@link Point} on the {@link Straight}. * @param other * The {@link Straight} to test. * @return <code>true</code> if the two {@link Straight}s intersect and the * intersection {@link Point} is contained within the specified * segment, otherwise <code>false</code> */ public boolean intersectsWithinSegment(Vector segmentStart, Vector segmentEnd, Straight other) { // precondition: segment start and end have to be points on this // straight. if (!contains(segmentStart) || !contains(segmentEnd)) { throw new IllegalArgumentException( "segment points have to be contained"); //$NON-NLS-1$ } // check if segmentStart->segmentEnd is a legal segment or a single // point Vector segmentDirection = segmentEnd.getSubtracted(segmentStart); if (segmentDirection.isNull()) { return other.contains(segmentStart); } // legal segment, check if there is an intersection within the segment if (intersects(other)) { Vector intersection = getIntersection(other); return containsWithinSegment(segmentStart, segmentEnd, intersection); } return false; } /** * Checks if this {@link Straight} and the provided one are parallel to each * other. Identical {@link Straight}s are regarded to be parallel to each * other. * * @param other * the {@link Straight} that is checked to be parallel to this * {@link Straight} * @return <code>true</code> if the direction {@link Vector}s of this * {@link Straight} and the provided one are parallel, otherwise * <code>false</code> */ public boolean isParallelTo(Straight other) { return direction.isParallelTo(other.direction); } /** * @see java.lang.Object#toString() */ @Override public String toString() { return "Straight: " + position.toString() + " + s * " //$NON-NLS-1$ + direction.toString(); } }