Calculating if CGPoint is on a line segment?

Viewed 147

I'm looking for a way to calculate if a CGPoint is on a line segment. Are there any built-in methods in Swift or any of Apple's libraries?

I don't mind converting to a different type as long as it is part of Apple's code (e.g. similar to how instead of performing complex calculations on double4x4 matrix, you can convert it to an SCNNode).


I quickly came up with brute force solution, but it's neither elegant, nor accurate (given floating point comparisons everywhere):

extension CGPoint {
  func isOnLineSegment(start: CGPoint, end: CGPoint) -> Bool {
    func value(_ value: CGFloat, isBetween one: CGFloat, and two: CGFloat) -> Bool {
      let a = [one, two].sorted()
      return a[0] <= value && value <= a[1]
    }

    let slope = (end.y - start.y) / (end.x - start.x)
    if slope.isInfinite {
      let isOnLine = x.isNearlyEqual(to: start.x)
      let isWithinYValues = value(y, isBetween: start.y, and: end.y)
      return isOnLine && isWithinYValues
    }

    let isOnLine = y.isNearlyEqual(to: slope * (x - start.x) + start.y) // y = m * (x - x1) + y1
    let isWithinXValues = value(x, isBetween: start.x, and: end.x)

    return isOnLine && isWithinXValues
  }
}
2 Answers

A possible approach is to transform the whole configuration such that the start point of the segment is transformed to the origin, and the end point is transformed to the point (1, 0). This can be done with a CGAffineTransformation. The problem then reduces to determining if the given point is on the line segment between (0, 0) and (1,0). Here is a possible implementation:

extension CGPoint {
    func isOnLineSegment(start: CGPoint, end: CGPoint) -> Bool {
        // Transformation which maps (0,0) to start and (1, 0) to end:
        let t = CGAffineTransform(a: end.x - start.x, b: end.y - start.y,
                                  c: start.y - end.y, d: end.x - start.x,
                                  tx: start.x, ty: start.y)
        // Apply the inverse transformation to our point:
        let q = self.applying(t.inverted())
        // Check (with some tolerance) if q is on the segment from (0, 0) to (1, 0):
        let eps = CGFloat.ulpOfOne.squareRoot()
        return q.x > -eps && q.x < 1.0 + eps && q.y.magnitude < eps 
    }
}

As tolerance I chose the square root of .ulpOfOne (as suggested here), but you can adjust it to your needs.

Another approach is to use the fact that a point P is on the line segment from P1 to P2 exactly if

| P - P1 | + | P - P2 | = | P1 - P2 |

where | . | denotes the (Euclidean) distance:

extension CGPoint {
    func distance(to p: CGPoint) -> CGFloat {
        return hypot(p.x - self.x, p.y - self.y)
    }
    func isOnLineSegment(start: CGPoint, end: CGPoint) -> Bool {
        let eps = CGFloat.ulpOfOne.squareRoot()
        return self.distance(to: start) + self.distance(to: end) <= start.distance(to: end) + eps
    }
}

If you fear floating-point inaccuracies, you'd better compute the distance to the line segment and check if it falls under a tolerance. (I will not expand on how to select this tolerance.)

An efficient computation scheme is as follows, using complex numbers (I use them for convenience, but there are equivalent vector forms, or just scalar expressions):

  • translate one of the endpoints to the origin (now the segment is 0 to P1-P0 and the tested point is Q-P0);

  • rotate to make the segment horizontal; for this, you multiply by (R:= (P1-P0)*/|P1-P0|, where * denotes the complex conjugate.

  • now the real part of (Q-P0).R tells you if the point projects to the segment (when 0≤Im≤|P1-P0|).

    • if yes, the imaginary part is the distance to the segment;

    • if no, compute the distance to the nearest endpoint (you know which by looking at the above real part).

The picture illustrates the three different cases to get the distance to the segment (transformed points).

enter image description here


Technical notes:

  1. Depending on your needs, you can adopt different strategies for the handling of the endpoints:
  • a point past the endpoint can be considered outside the segment;

  • a point past the endpoint horizontally by more than the tolerance can be considered outside the segment;

  • a point past the endpoint obliquely by more than the tolerance can be considered outside the segment.

These options correspond to acceptance domains with respective shapes being tight rectangle, larger rectangle and rounded rectangle.

  1. This method preserves scale, making the tolerance an absolute distance criterion.
Related