Skip to content

Snap to Polyline

Finds the nearest point on a decoded route polyline to a user's click on the admin map. Used during route building to snap user-placed waypoints onto the existing road geometry.


Where It Is Used

apps/web-admin/src/app/utils/polyline.tssnapToPolyline(click, polyline)

Called in the line-form and route-form components whenever the admin clicks the map to add a waypoint or checkpoint.


Algorithm

For each segment $A \to B$ of the polyline, project the click point $P$ perpendicularly onto the segment:

     P (click)
     |
     |  d (distance)
     v
A----P'----B  (polyline segment)

Return the $P'$ with the minimum distance across all segments.

Steps:

  1. Iterate over consecutive segment pairs $(A_i, A_{i+1})$.
  2. Compute the projection parameter: $$t = \frac{(\vec{AP}) \cdot (\vec{AB})}{|\vec{AB}|^2}$$
  3. Clamp to $[0, 1]$ so the result stays on the segment, not the infinite line.
  4. Compute the projected point: $P' = A + t_{\text{clamped}} \cdot (B - A)$.
  5. Calculate the squared 2D Euclidean distance from $P$ to $P'$ (square avoids the sqrt cost for comparison).
  6. Track the segment yielding the minimum squared distance.
  7. Return $P'$ on that segment.

Implementation

function snapToPolyline(
  click: [number, number],
  polyline: [number, number][]
): [number, number] {
  let best: [number, number] = polyline[0];
  let bestDist = Infinity;

  for (let i = 0; i < polyline.length - 1; i++) {
    const p = closestOnSegment(click, polyline[i], polyline[i + 1]);
    const d = distSq(click, p);
    if (d < bestDist) { bestDist = d; best = p; }
  }
  return best;
}

function closestOnSegment(
  p: [number, number],
  a: [number, number],
  b: [number, number]
): [number, number] {
  const dx = b[0] - a[0], dy = b[1] - a[1];
  const lenSq = dx * dx + dy * dy;
  if (lenSq === 0) return a;
  const t = Math.max(0, Math.min(1,
    ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / lenSq
  ));
  return [a[0] + t * dx, a[1] + t * dy];
}

function distSq(a: [number, number], b: [number, number]): number {
  return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2;
}

Note: This uses 2D Euclidean distance (degree-space), not Haversine, because the admin UI only needs relative ranking between segments on the same route — the small-angle approximation is acceptable here.