Proximity Searching by Drive Distance (and Drive Time) in MWS
I’ve discovered a way to easily (and cheaply) improve store locator relevancy by using a driving distance calculation to determine the closest locations to a user instead of as the crow flies. “As the crow flies?” you ask. The FindNearby() method in MapPoint Web Service uses a calculation of the Earth coordinates system to determine which locations are closest based on a algorithmic determinations. Uh, what?
Let’s use an example – always helpful. Let’s say I’m looking for the nearest Best Buy. A typical store locator will calculate that the nearest best to my house is 5 miles away, but when I actually drive to that Best Buy it’s 8 miles away. I spent more money on gas then I thought I would. Plus, there’s a Best Buy that the locator said was 6 miles away in the other direction. I went to the one that was 5 miles away because I thought it was closer, but that’s because the calculation using the coordinate system doesn’t take into consideration turns in the road or the lake I need to go around to get to the main road. The classic scenario is that the closest location is just 1 mile across the lake – but the bridge is 5 miles up the road, 1 mile across and 5 miles down the road on the other side! So, let’s fix this.
The first thing I would instinctively do is calculate the driving directions to each of the nearest Best Buy’s. If I get 10 results for my search, let’s count transactions . . . 1 transaction for geocoding the end user’s address, 10 transactions for calculating the distance to each location – that’s 11 transactions per search! If I want a map, throw in another transaction. Too expensive, not to mention the bandwidth and latency concerns (up to 5 seconds per round in tandem – 50 seconds – no way).
I’ve got a better solution that only uses 3 transactions. The code below illustrates that you can find the nearest locations using FindAddress(), FindNearby() and CalculateSimpleRoute() - 3 transactions to solve your end user’s heartburn and save some money on gas – not to mention decrease the number of angry phone calls your support people manage because of it.
So, here’s how to do it . . .
First geocode the user’s address using the FindAddress() (or Find()) method.
//Yes, it's only in C# :)
FindResults findAddressResults = findService.FindAddress(findAddressSpec);
Next, use FindNearby() to find locations within a specific radius. You’ll want this to be larger than normal since you want to capture as many locations as possible.
FindResults findNearbyResults = findService.FindNearby(findNearbySpec);
//Loop through results and stick information into an array
for (int p = 0; p < findNearbyResults.Results.Length; p++)
{
StringBuilder sbCrow = new StringBuilder();
sbCrow.Append(findNearbyResults.Results[p].FoundLocation.Entity.Properties[0].Value.ToString());
arrLocationInfo[p] = sbCrow.ToString();
}
Here’s the money shot. Calculate the routes between each of your results using CalculateSimpleRoute(); however, instead of routing to each location in subsequent requests, you’ll route from the user’s address to the first location, back to the user’s address then to the second location back to the user’s address then to the third location, etc. CalculateSimpleRoute() supports up to 50 points in a route, so you’ll be able to do this for up to 25 locations! Once you get the results back from the routing algorithm, you’ll want to stick them into an array and sort based on driving distance (and drive time).
RouteServiceSoap routeService = Services.routeservice();
/* Store twice as many lat lons to account for returning back to user’s address */
LatLong[] myLL = new LatLong[(findNearbyResults.Results.Length * 2)];
/* Delta represents which part of the route we’re on – going to location or returning back to user’s address */
int delta = 0;
/* Loop through FindNearby Results to create array to send to CalculateSimpleRoute() */
for (int x = 0; x < (findNearbyResults.Results.Length * 2); x++)
{
RouteSpecification routeSpec = new RouteSpecification();
routeSpec.DataSourceName = "MapPoint.NA";
myLL[x] = new LatLong();
myLL[x].Latitude = findAddressResults.Results[0].FoundLocation.LatLong.Latitude;
myLL[x].Longitude = findAddressResults.Results[0].FoundLocation.LatLong.Longitude;
delta++;
x++;
myLL[x] = new LatLong();
myLL[x].Latitude = findNearbyResults.Results[x-delta].FoundLocation.LatLong.Latitude;
myLL[x].Longitude = findNearbyResults.Results[x-delta].FoundLocation.LatLong.Longitude;
}
/* Send the array of points from user’s address to each location and back – send to CalculateSimpleRoute() */
Route myRoute = routeService.CalculateSimpleRoute(myLL, "MapPoint.NA", SegmentPreference.Shortest);
//Create array of distances and drive times.
//Using Single dim arrays - Driving Distance.
double[] arrDistances = new double[findNearbyResults.Results.Length];
//Using Single dim arrays – Driving Time.
long[] arrTime = new long[findNearbyResults.Results.Length];
for (int d = 0; d < (myRoute.Itinerary.Segments.Length / 2); d++)
{
arrDistances[d] = myRoute.Itinerary.Segments[d+d].Distance;
arrTime[d] = myRoute.Itinerary.Segments[d+d].DrivingTime;
}
//Sort the arrays based on drive distance
Array.Sort(arrDistances, arrLocationInfo);
//Sort the arrays based on drive time
Array.Sort(arrTime, arrLocationInfo);
From here you can take your arrays and parse them into a table and display it on the page, throw some pushpins onto a map or whatever. So simple! I threw drive time in for fun. I’m not how useful it is, but it’s returned with the RouteResults so I figured what’s one more line of code?
CP