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.ts — snapToPolyline(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:
- Iterate over consecutive segment pairs $(A_i, A_{i+1})$.
- Compute the projection parameter: $$t = \frac{(\vec{AP}) \cdot (\vec{AB})}{|\vec{AB}|^2}$$
- Clamp to $[0, 1]$ so the result stays on the segment, not the infinite line.
- Compute the projected point: $P' = A + t_{\text{clamped}} \cdot (B - A)$.
- Calculate the squared 2D Euclidean distance from $P$ to $P'$ (square avoids the
sqrtcost for comparison). - Track the segment yielding the minimum squared distance.
- 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.
Related
- Google Encoded Polyline — polyline is decoded before snapping
- Geodesic Line-Segment Projection Snapping — the same projection math applied geodesically for passenger routing