1

I want to decide whether a mouse click point lies on an SVG polyline. I found this Python code to determine whether a point lies between two other points, and reimplemented it in JavaScript.

function isOnLine(xp, yp, x1, y1, x2, y2){

    var p = new Point(x, y);
    var epsilon = 0.01;

    var crossProduct = (yp - y1) * (x2 - x1) - (xp - x1) * (y2 - y1);
    if(Math.abs(crossProduct) > epsilon)
        return false;
    var dotProduct = (xp - x1) * (x2 - x1) + (yp - y1)*(y2 - y1);
    if(dotProduct < 0)
        return false;
    var squaredLengthBA = (x2 - x1)*(x2 - x1) + (y2 - y1)*(y2 - y1);
    if(dotProduct > squaredLengthBA)
        return false;

    return true;
}

But it's not working the way I want, because I will never get the mouse pointer exactly on the line. So I need something like an "imaginary thick line" to get some margin of error:

enter image description here

Any ideas?

Community
  • 1
  • 1
tprieboj
  • 1,680
  • 6
  • 31
  • 54

1 Answers1

7

The cross product divided by the length of the line gives the distance of the point from the line. So just compare this to some threshold value:

function isOnLine (xp, yp, x1, y1, x2, y2, maxDistance) {
    var dxL = x2 - x1, dyL = y2 - y1;  // line: vector from (x1,y1) to (x2,y2)
    var dxP = xp - x1, dyP = yp - y1;  // point: vector from (x1,y1) to (xp,yp)

    var squareLen = dxL * dxL + dyL * dyL;  // squared length of line
    var dotProd   = dxP * dxL + dyP * dyL;  // squared distance of point from (x1,y1) along line
    var crossProd = dyP * dxL - dxP * dyL;  // area of parallelogram defined by line and point

    // perpendicular distance of point from line
    var distance = Math.abs(crossProd) / Math.sqrt(squareLen);

    return (distance <= maxDistance && dotProd >= 0 && dotProd <= squareLen);
}

Ps. The code above will essentially expand the line segment into a box by widening it by maxDistance on either side, and accept any clicks within that box. If you're going to apply this to a polyline (i.e. multiple line segments joined end-to-end), you may find that there are gaps between these boxes where two line segments meet at an angle:

Illustration of expanded polyline with gaps

A simple and natural way to fix this is to also accept any clicks anywhere within a radius of maxDistance from the endpoints, essentially padding the imaginary box with (semi)circular end caps:

Illustration of expanded polyline with end caps joining segments

Here's one way to implement this:

function isOnLineWithEndCaps (xp, yp, x1, y1, x2, y2, maxDistance) {
    var dxL = x2 - x1, dyL = y2 - y1;  // line: vector from (x1,y1) to (x2,y2)
    var dxP = xp - x1, dyP = yp - y1;  // point: vector from (x1,y1) to (xp,yp)
    var dxQ = xp - x2, dyQ = yp - y2;  // extra: vector from (x2,y2) to (xp,yp)

    var squareLen = dxL * dxL + dyL * dyL;  // squared length of line
    var dotProd   = dxP * dxL + dyP * dyL;  // squared distance of point from (x1,y1) along line
    var crossProd = dyP * dxL - dxP * dyL;  // area of parallelogram defined by line and point

    // perpendicular distance of point from line
    var distance = Math.abs(crossProd) / Math.sqrt(squareLen);

    // distance of (xp,yp) from (x1,y1) and (x2,y2)
    var distFromEnd1 = Math.sqrt(dxP * dxP + dyP * dyP);
    var distFromEnd2 = Math.sqrt(dxQ * dxQ + dyQ * dyQ);

    // if the point lies beyond the ends of the line, check if
    // it's within maxDistance of the closest end point
    if (dotProd < 0) return distFromEnd1 <= maxDistance;
    if (dotProd > squareLen) return distFromEnd2 <= maxDistance;

    // else check if it's within maxDistance of the line
    return distance <= maxDistance;
}
Ilmari Karonen
  • 49,047
  • 9
  • 93
  • 153