/******************************************************************************* * Copyright (c) 2011, 2016 itemis AG 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 (itemis AG) - initial API and implementation * Matthias Wienand (itemis AG) - contribution for Bugzilla #355997 * Colin Sharples - contribution for Bugzilla #460754 * *******************************************************************************/ package org.eclipse.gef.geometry.planar; import java.util.HashSet; import java.util.Set; import org.eclipse.gef.geometry.euclidean.Angle; import org.eclipse.gef.geometry.euclidean.Straight; import org.eclipse.gef.geometry.euclidean.Vector; import org.eclipse.gef.geometry.internal.utils.PointListUtils; import org.eclipse.gef.geometry.internal.utils.PrecisionUtils; import org.eclipse.gef.geometry.projective.Vector3D; /** * Represents the geometric shape of a line (or linear curve). * * Note that while all manipulations (e.g. within shrink, expand) within this * class are based on double precision, all comparisons (e.g. within contains, * intersects, equals, etc.) are based on a limited precision (with an accuracy * defined within {@link PrecisionUtils}) to compensate for rounding effects. * * @author anyssen * @author mwienand * */ public class Line extends BezierCurve { private static final long serialVersionUID = 1L; /** * Constructs a new {@link Line} from the given coordinate values. * * @param coordinates * A varargs of 4 doubles, providing the x and y coordinates of * the start point, followed by those of the end point * @see BezierCurve#BezierCurve(double[]) */ public Line(double... coordinates) { super(coordinates); if (coordinates.length != 4) { throw new IllegalArgumentException( "A Line may only be defined by 4 coordinates (2 points), while " + coordinates.length + " were passed in."); } } /** * Constructs a new {@link Line}, which connects the two {@link Point}s * given indirectly by their coordinates * * @param x1 * the x-coordinate of the start point * @param y1 * the y-coordinate of the start point * @param x2 * the x-coordinate of the end point * @param y2 * the y-coordinate of the end point */ public Line(double x1, double y1, double x2, double y2) { super(x1, y1, x2, y2); } /** * Constructs a new {@link Line}, which connects the two {@link Point}s * given. * * @param points * A varargs of two points serving as the start and end point of * this line */ public Line(Point... points) { super(points); if (points.length != 2) { throw new IllegalArgumentException( "A Line may only be defined by two points, while " + points.length + " were passed in."); } } /** * Constructs a new {@link Line} which connects the two given {@link Point}s * * @param p1 * the start point * @param p2 * the end point */ public Line(Point p1, Point p2) { this(p1.x, p1.y, p2.x, p2.y); } @Override public boolean contains(Point p) { if (p == null) { return false; } if (getP1().equals(p) || getP2().equals(p)) { return true; } double distance = Math.abs(new Straight(getP1(), getP2()) .getSignedDistanceCCW(new Vector(p))); return PrecisionUtils.equal(distance, 0) && getBounds().contains(p); } /** * Tests whether this {@link Line} is equal to the line given implicitly by * the given point coordinates. * * @param x1 * the x-coordinate of the start point of the line to test * @param y1 * the y-coordinate of the start point of the line to test * @param x2 * the x-coordinate of the end point of the line to test * @param y2 * the y-coordinate of the end point of the line to test * @return <code>true</code> if the given start and end point coordinates * are (imprecisely) equal to this {@link Line} 's start and end * point coordinates */ public boolean equals(double x1, double y1, double x2, double y2) { return PrecisionUtils.equal(x1, getX1()) && PrecisionUtils.equal(y1, getY1()) && PrecisionUtils.equal(x2, getX2()) && PrecisionUtils.equal(y2, getY2()) || PrecisionUtils.equal(x2, getX1()) && PrecisionUtils.equal(y2, getY1()) && PrecisionUtils.equal(x1, getX2()) && PrecisionUtils.equal(y1, getY2()); } @Override public Point get(double t) { // XXX: Overwritten to improve performance for the Line case. if (t < 0 || t > 1) { throw new IllegalArgumentException("t out of range: " + t); } return new Point(((1 - t) * getX1() + t * getX2()), ((1 - t) * getY1() + t * getY2())); } /** * Returns the smallest {@link Rectangle} containing this {@link Line}'s * start and end point * * @see IGeometry#getBounds() */ @Override public Rectangle getBounds() { return new Rectangle(getP1(), getP2()); } /** * Returns a new {@link Line}, which has the same start and end point * coordinates as this one. * * @return a new {@link Line} with the same start and end point coordinates */ @Override public Line getCopy() { return new Line(getP1(), getP2()); } /** * Returns the counter-clockwise angle between the x axis and this * {@link Line}. * * @return Returns the counter-clockwise angle between the x axis and this * {@link Line}. */ public Angle getDirectionCCW() { Point start = getP1(); Point end = getP2(); return Angle.fromRad(Math.atan2(end.y - start.y, end.x - start.x)); } /** * Returns the clockwise angle between the x axis and this {@link Line}. * * @return Returns the clockwise angle between the x axis and this * {@link Line}. */ public Angle getDirectionCW() { return getDirectionCCW().getOppositeFull(); } /** * Returns the single intersection point between this {@link Line} and the * given one, in case it exists. Note that even in case * {@link Line#intersects} returns true, there may not be a single * intersection point in case both lines overlap in more than one point. * * @param l * the Line, for which to compute the intersection point * @return the single intersection point between this {@link Line} and the * given one, in case it intersects, <code>null</code> instead */ public Point getIntersection(Line l) { Point p1 = getP1(); Point p2 = getP2(); // degenerated case if (p1.equals(p2)) { if (l.contains(p1)) { return p1; } else if (l.contains(p2)) { return p2; } return null; } Point lp1 = l.getP1(); Point lp2 = l.getP2(); // degenerated case if (lp1.equals(lp2)) { if (contains(lp1)) { return lp1; } else if (contains(lp2)) { return lp2; } return null; } Straight s1 = new Straight(p1, p2); Straight s2 = new Straight(lp1, lp2); if (s1.isParallelTo(s2)) { Vector vlp1 = new Vector(lp1); Vector vlp2 = new Vector(lp2); if (s1.contains(vlp1) && s1.contains(vlp2)) { // end-point-intersection? (no overlap) double u1 = s1.getParameterAt(vlp1); double u2 = s1.getParameterAt(vlp2); if (PrecisionUtils.equal(u1, 0) && u2 < u1 || PrecisionUtils.equal(u1, 1) && u2 > u1) { return lp1; } else if (PrecisionUtils.equal(u2, 0) && u1 < u2 || PrecisionUtils.equal(u2, 1) && u1 > u2) { return lp2; } } return null; } Point intersection = s1.getIntersection(s2).toPoint(); return contains(intersection) && l.contains(intersection) ? intersection : null; } @Override protected Set<IntervalPair> getIntersectionIntervalPairs(BezierCurve other, Set<Point> intersections) { if (other instanceof Line) { return getIntersectionIntervalPairs((Line) other, intersections); } return super.getIntersectionIntervalPairs(other, intersections); } /** * Provides an optimized version of the * {@link BezierCurve#getIntersectionIntervalPairs(BezierCurve, Set)} * method. * * @param other * The {@link Line} which is searched for points of intersections * with this {@link BezierCurve}. * @param intersections * The {@link Set} where intersections are inserted. * @return see * {@link BezierCurve#getIntersectionIntervalPairs(BezierCurve, Set)} */ protected Set<IntervalPair> getIntersectionIntervalPairs(Line other, Set<Point> intersections) { HashSet<IntervalPair> intervalPairs = new HashSet<>(); Straight s1 = new Straight(this); Straight s2 = new Straight(other); Vector vi = s1.getIntersection(s2); if (vi != null) { Point poi = vi.toPoint(); if (contains(poi) && other.contains(poi)) { double param1 = s1.getParameterAt(vi); double param2 = s2.getParameterAt(vi); param1 = param1 < 0 ? 0 : param1 > 1 ? 1 : param1; param2 = param2 < 0 ? 0 : param2 > 1 ? 1 : param2; intervalPairs.add( new IntervalPair(this, new Interval(param1, param1), other, new Interval(param2, param2))); } } return intervalPairs; } @Override public Point[] getIntersections(BezierCurve curve) { if (curve instanceof Line) { Point poi = getIntersection((Line) curve); if (poi != null) { return new Point[] { poi }; } return new Point[] {}; } return super.getIntersections(curve); } // TODO: add specialized getOverlap() /** * Calculates the distance between the {@link Point start} and the * {@link Point end point} of this {@link Line}. * * @see Point#getDistance(Point) * @return The distance between start and end points. */ public double getLength() { return getP1().getDistance(getP2()); } /** * Returns an array, which contains two {@link Point}s representing the * start and end points of this {@link Line} * * @return an array with two {@link Point}s, whose x and y coordinates match * those of this {@link Line}'s start and end point */ @Override public Point[] getPoints() { return new Point[] { getP1(), getP2() }; } @Override public Point getProjection(Point p) { // XXX: If this line is degenerated to a point (i.e. start equals end // point) then we can return the start or end point as the projection. // The default computation (see below) cannot handle this case, as a // straight needs to have a direction (which cannot be determined for a // degenerated line). if (getP1().equals(getP2())) { return getP1(); } Straight s = new Straight(this); Point projected = s.getProjection(new Vector(p)).toPoint(); // XXX: We can use our bounds to do a simple containment test here, as // the point was already projected onto the straight through this line. // If its not located within the bounds, its not on this line and we // have to return start or end point as nearest point. if (Double.isNaN(projected.x) || Double.isNaN(projected.y) || Double.isInfinite(projected.x) || Double.isInfinite(projected.y)) { return p; } if (getP1().equals(getP2()) && (getP1().equals(projected) || getP2().equals(projected)) || getBounds().contains(projected)) { return projected; } else { return Point.nearest(projected, getPoints()); } } /** * @see IGeometry#getTransformed(AffineTransform) */ @Override public Line getTransformed(AffineTransform t) { return new Line(t.getTransformed(getPoints())); } @Override public boolean intersects(ICurve c) { if (c instanceof Line) { return intersects((Line) c); } return super.intersects(c); } /** * Provides an optimized version of the * {@link BezierCurve#intersects(ICurve)} method. * * @param l * The {@link Line} to test for intersections. * @return see {@link BezierCurve#intersects(ICurve)} */ public boolean intersects(Line l) { return getIntersection(l) != null; } @Override public boolean overlaps(BezierCurve c) { if (c instanceof Line) { return overlaps((Line) c); } // BezierCurve: in order to overlap, all control points have to lie on a // straight through its base line Straight s = new Straight(this); for (Line seg : PointListUtils.toSegmentsArray(c.getPoints(), false)) { if (!s.equals(new Straight(seg))) { return false; } } // if the base line overlaps, we are done if (overlaps(new Line(c.getP1(), c.getP2()))) { return true; } else { // otherwise, we have to delegate to the general implementation for // Bezier curves to take care of a degenerated curve, where the // handle points are outside the base line of the Bezier curve. return super.touches(c); } } /** * Tests whether this {@link Line} and the given other {@link Line} overlap, * i.e. they share an infinite number of {@link Point}s. * * @param l * the other {@link Line} to test for overlap with this * {@link Line} * @return <code>true</code> if this {@link Line} and the other {@link Line} * overlap, otherwise <code>false</code> * @see ICurve#overlaps(ICurve) */ public boolean overlaps(Line l) { return touches(l) && !intersects(l); } /** * Initializes this {@link Line} with the given start and end point * coordinates * * @param x1 * the x-coordinate of the start point * @param y1 * the y-coordinate of the start point * @param x2 * the x-coordinate of the end point * @param y2 * the y-coordinate of the end point * @return <code>this</code> for convenience */ public Line setLine(double x1, double y1, double x2, double y2) { setP1(new Point(x1, y1)); setP2(new Point(x2, y2)); return this; } /** * Initializes this {@link Line} with the start and end point coordinates of * the given one. * * @param l * the {@link Line} whose start and end point coordinates should * be used for initialization * @return <code>this</code> for convenience */ public Line setLine(Line l) { setLine(l.getX1(), l.getY1(), l.getX2(), l.getY2()); return this; } /** * Initializes this {@link Line} with the start and end point coordinates * provided by the given points * * @param p1 * the Point whose coordinates should be used as the start point * coordinates of this {@link Line} * @param p2 * the Point whose coordinates should be used as the end point * coordinates of this {@link Line} * @return <code>this</code> for convenience */ public Line setLine(Point p1, Point p2) { setLine(p1.x, p1.y, p2.x, p2.y); return this; } /** * Sets the x-coordinate of the start {@link Point} of this {@link Line} to * the given value. * * @param x1 * The new x-coordinate for the start {@link Point} of this * {@link Line}. * @return <code>this</code> for convenience */ public Line setX1(double x1) { setP1(new Point(x1, getY1())); return this; } /** * Sets the x-coordinate of the end {@link Point} of this {@link Line} to * the given value. * * @param x2 * The new x-coordiante for the end {@link Point} of this * {@link Line}. * @return <code>this</code> for convenience */ public Line setX2(double x2) { setP2(new Point(x2, getY2())); return this; } /** * Sets the y-coordinate of the start {@link Point} of this {@link Line} to * the given value. * * @param y1 * The new y-coordinate for the start {@link Point} of this * {@link Line}. * @return <code>this</code> for convenience */ public Line setY1(double y1) { setP1(new Point(getX1(), y1)); return this; } /** * Sets the y-coordinate of the end {@link Point} of this {@link Line} to * the given value. * * @param y2 * The new y-coordinate for the end {@link Point} of this * {@link Line}. * @return <code>this</code> for convenience */ public Line setY2(double y2) { setP2(new Point(getX2(), y2)); return this; } @Override public Path toPath() { Path path = new Path(); path.moveTo(getX1(), getY1()); path.lineTo(getX2(), getY2()); return path; } @Override public String toString() { return "Line: (" + getX1() + ", " + getY1() + ") -> (" + getX2() + ", " //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ + getY2() + ")"; //$NON-NLS-1$ } @Override public boolean touches(IGeometry g) { if (g instanceof Line) { return touches((Line) g); } return super.touches(g); } /** * Tests whether this {@link Line} and the given one share at least one * common point. * * @param l * The {@link Line} to test. * @return <code>true</code> if this {@link Line} and the given one share at * least one common point, <code>false</code> otherwise. */ public boolean touches(Line l) { // TODO: optimize w.r.t. object creation /* * 1) check degenerated (the start and end point imprecisely fall * together) and special cases where the lines have to be regarded as * intersecting, because they touch within the used imprecision, though * they would not intersect with absolute precision. */ Point p1 = getP1(); Point p2 = getP2(); boolean touches = l.contains(p1) || l.contains(p2); if (touches || p1.equals(p2)) { return touches; } Point lp1 = l.getP1(); Point lp2 = l.getP2(); touches = contains(lp1) || contains(lp2); if (touches || lp1.equals(lp2)) { return touches; } Vector3D l1 = new Vector3D(p1).getCrossProduct(new Vector3D(p2)); Vector3D l2 = new Vector3D(lp1).getCrossProduct(new Vector3D(lp2)); /* * 2) non-degenerated case. If the two respective straight lines * intersect, the intersection has to be contained by both line segments * for the segments to intersect. If the two respective straight lines * do not intersect, because they are parallel, the getIntersection() * method returns null. */ Point intersection = l1.getCrossProduct(l2).toPoint(); return intersection != null && contains(intersection) && l.contains(intersection); } }