Before we even consider ordering the results by distance from route, we have to have some basic functions to calculate distance. We immediately run into a snag here, because of course the earth is a sphere, and distances on a sphere are more complicated than distances on a plane.
However, over relatively small distances (Tamiami to Miami, for example), we can treat distances as if they were on a plane, and achieve reasonable results. If we can live with that approximation, we simply need a method for determining the minimum distance between a point and a line segment. Instead of re-inventing the wheel, I adapted some code from this SO answer:
function sqr(x) { return x * x }
function dist2(v, w) { return sqr(v.x - w.x) + sqr(v.y - w.y) }
function distToSegment2(p, v, w) {
return dist2(getClosestPoint(p,v,w));
}
function getClosestPoint( p, v, w ) {
var l2 = dist2(v, w);
if (l2 === 0) return v; // line is actually a point; just return one ofthe two points
var t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2;
// point is closest to v, return v
if (t < 0) return v;
// point is closest to w, return w
if (t > 1) return w;
// point is closets to some midpoint, return that
return { x: v.x + t * (w.x - v.x), y: v.y + t * (w.y - v.y) };
}
function distToSegment(p, v, w) { return Math.sqrt(distToSegmentSquared(p, v, w)); }
Now since all we're using the distance for (currently) is sorting, we don't have to take the extra step of taking the square root, so we can simply use the dist2
(distance squared) function to save us a little extra computation.
The results from a Google route query contain an array called overview_path
which contains all of the line segments used to actually draw the path on the map. We'll use these segments to determine the closest point:
function closestPointOnPath_Cartesian( place, path, cb ) {
var min = Number.MAX_VALUE;
var closestPoint = null;
for( var i=0; i<path.length-1; i++ ) {
var v = { x: path[i].lng(), y: path[i].lat() };
var w = { x: path[i+1].lng(), y: path[i+1].lat() };
var p1 = { x: place.geometry.location.lng(),
y: place.geometry.location.lat() };
var p2 = getClosestPoint( p1, v, w );
var d2 = dist2( p1, p2 );
if( d2 < min ) {
min = d2;
closestPoint = new google.maps.LatLng( p2.y, p2.x );
}
}
cb( closestPoint, min );
}
Note that I was careful to name this function to indicate that this calculates Cartesian distance, which could cause problems with long routes, or routes close to the poles.
Now that we have this function, all we have to do is annotate each result with the distance so we can sort it, and then actually perform the sort:
for( var i=0; i<results.length; i++ ) {
closestPointOnPath_Cartesian( results[i],
result.routes[0].overview_path,
function( closestPoint, coordDist2 ){
results[i].closestPointOnPath = closestPoint;
results[i].coordDist2 = coordDist2;
results[i].geoDistKm = geoDistanceKm( results[i].geometry.location, closestPoint );
});
}
// sort results by relative distance (coordDist2)
results.sort( function(a,b) { return a.coordDist2 - b.coordDist2; } );
For debugging visualization purposes, I added some code to draw the line from the closest point on the path to each location:
var distLine = new google.maps.Polyline({
path: [place.closestPointOnPath, place.geometry.location],
strokeColor: '#ff0000',
strokeOpacity: 1.0,
strokeWeight: 2
});
distLine.setMap( map );
Lastly, for extra credit, I used the haversine formula (adapted from this SO answer)
Number.prototype.toRad = function() {
return this * Math.PI / 180;
}
// geographic distance courtesy the haversine formula
function geoDistanceKm(p1,p2) {
var R = 6371; // km
var x1 = p2.lat()-p1.lat();
var dLat = x1.toRad();
var x2 = p2.lng()-p1.lng();
var dLon = x2.toRad();
var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(p1.lat().toRad()) * Math.cos(p2.lat().toRad()) *
Math.sin(dLon/2) * Math.sin(dLon/2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
}
So now we can actually display true geographic distance in the results list.
Note: because we're computing distance on the plane, for long routes, or routes near the pole, it is entirely possible that the order will NOT match the (correct) order based on the geographic distance as determined by the haversine formula. To fix this would require deriving an algorithm to determine point/segment distance on a sphere, which is a task for another day.
You can see the complete solution working here: http://jsfiddle.net/UqLTE/4/