Driving Distance

As I cannot find any method to applescript the Maps application, how can I find driving distances between two addresses?

Based upon a prior wonderful discussion of scripting MapKit [topic]45915[/topic], I attempted to set a MapKit direction request source location to a SourceMKMapItem.


use framework "Foundation"
use framework "MapKit" --required for  MKPlacemark
use scripting additions

set SourceAddressString to "Rockefeller Plaza, NY, NY"
set {theLat, theLong, fullAddress} to (my coordinatesForAddress:(contents of SourceAddressString))
set SourceMKMapItem to my MapItemWithPlacemarkFromAddress:fullAddress latitude:theLat longitude:theLong
my MKDirectionsRequestFrom:SourceMKMapItem


on MapItemWithPlacemarkFromAddress:fullAddress latitude:theLat longitude:theLong
	#	Get address components
	set fullAddress to current application's NSString's stringWithString:fullAddress
	set DataDetector to current application's NSDataDetector
	set DataDetectorOfAddress to DataDetector's dataDetectorWithTypes:(current application's NSTextCheckingTypeAddress) |error|:(missing value)
	set FirstMatchOfDataDetectorAddress to DataDetectorOfAddress's firstMatchInString:fullAddress options:0 range:{0, fullAddress's |length|()}
	if FirstMatchOfDataDetectorAddress = missing value then error "Can't interpret address"
	
	#	Create a dictionary of returned key values relative to this data detector object
	set addressDictionay to FirstMatchOfDataDetectorAddress's components()
	
	#	Create a placemark using the coordinate and address dictionary
	set aCoordinate to {latitude:theLat, longitude:theLong}
	set aPlacemark to (current application's MKPlacemark's alloc()'s initWithCoordinate:aCoordinate addressDictionary:addressDictionay)
	
	#	Create a map item initiating with the placemark
	set MapItemWithPlacemark to (current application's MKMapItem's alloc()'s initWithPlacemark:aPlacemark)
end MapItemWithPlacemarkFromAddress:latitude:longitude:

on coordinatesForAddress:AddressString
	set AddressString to current application's NSString's stringWithString:AddressString
	set AddressString to AddressString's stringByReplacingOccurrencesOfString:space withString:"+"
	set urlString to "htt" & "ps://maps.googleapis.com/maps/api/geocode/json?address=" & AddressString
	set theURL to current application's |NSURL|'s URLWithString:urlString
	set theData to current application's NSData's dataWithContentsOfURL:theURL
	set {theJson, theError} to (current application's NSJSONSerialization's JSONObjectWithData:theData options:0 |error|:(reference))
	if theJson is missing value then error theError's localizedDescription() as text
	set theStatus to theJson's objectForKey:"status" -- check it went OK
	if not (theStatus's isEqualToString:"OK") then error theStatus as text
	set theResults to theJson's objectForKey:"results"
	set thePred to current application's NSPredicate's predicateWithFormat:"types CONTAINS 'street_address'"
	set bestResult to (theResults's filteredArrayUsingPredicate:thePred)'s firstObject()
	if bestResult = missing value then set bestResult to theResults's firstObject()
	set theLat to (bestResult's valueForKeyPath:"geometry.location.lat") as real
	set theLong to (bestResult's valueForKeyPath:"geometry.location.lng") as real
	set fullAddress to (bestResult's objectForKey:"formatted_address") as text
	return {theLat, theLong, fullAddress}
end coordinatesForAddress:

on MKDirectionsRequestFrom:SourceMKMapItem
	set directionsRequest to current application's MKDirectionsRequest
	set directionsRequest to directionsRequest's setSource:SourceMKMapItem
end MKDirectionsRequestFrom:

My applescript errs on the following command:

set directionsRequest to directionsRequest's setSource:SourceMKMapItem

with the following error:

  1. 1. What is the correct method to set the source property for MapKit’s DirectionRequest using a MapItem?[/*]
  2. 2. As my overall goal is to applescript driving distances between two geographical locations, what might be another method?[/*]

Having found no MapKit method to applescript driving distances, I scripted MapQuest’s Directions API www.mapquestapi.com/directions. MapQuest’s API allows free entry level accounts, that will provide API keys to its URL. I have substitued an imaginary key to demonstrate my applescript in to prepare a url encoded MapQuest API string.

use framework "Foundation"
use scripting additions

property MapquestAPIKey : "ABCDEFJHIJKLMNOP123456"

set fromLocation to "Rockefeller Center, NY, NY"
set toLocation to "Embarcadero Ferry Building, SF, CA"

set MapQuestURLResults to (my MapQuestDistanceFromSring:fromLocation toLocationSting:toLocation)

on MapQuestDistanceFromSring:fromLocation toLocationSting:toLocation
	set fromLocation to my PercentEncode:fromLocation
	set toLocation to my PercentEncode:toLocation
	set MapQuestURLlString to "http://www.mapquestapi.com/directions/v2/route" & ¬
		"?key=" & MapquestAPIKey & ¬
		"&from=" & fromLocation & ¬
		"&to=" & toLocation & ¬
		"&outFormat=json" & ¬
		"&ambiguities=ignore" & ¬
		"&routeType=fastest" & ¬
		"&doReverseGeocode=false" & ¬
		"&enhancedNarrative=false" & ¬
		"&avoidTimedConditions=false"
	set theURL to current application's |NSURL|'s URLWithString:MapQuestURLlString
end MapQuestDistanceFromSring:toLocationSting:

on PercentEncode:SourceString
	#	 Percent-escape all characters which are not  letter or digit.
	set SourceNSString to current application's class "NSString"'s stringWithString:(SourceString)
	set escapedSearchphrase to (SourceNSString's stringByAddingPercentEncodingWithAllowedCharacters:(current application's class "NSCharacterSet"'s alphanumericCharacterSet())) as text
end PercentEncode:

In a 33776 character url response, obviously detailed enough to navigate across the USA, a JSON object returned from the above script, that began with
{(NSDictionary) {route:{computedWaypoints:{}, …,.
Relying on Applescript’s Objective C NSJSONSerialization, I parsed the key value pair:
distance:2950.5544 as shown in the following script to derive the miles driving distance between the two locations:


set DistanceMiles to my ParseJsonDistanceMilesIn:MapQuestURLResults

on ParseJsonDistanceMilesIn:MapQuestURLlString
	set NSDataObject to current application's NSData
	set theData to NSDataObject's dataWithContentsOfURL:MapQuestURLlString
	set theData to current application's NSData's dataWithContentsOfURL:MapQuestURLlString
	
	#	NSJSONSerialization is an  object that converts between JSON and the equivalent Foundation object. In this case, the purpose is to convert from  JSON to an equivalent Foundation object.
	set JSONSerial to current application's NSJSONSerialization
	
	# 	JSONObjectWithData returns a Foundation object from given JSON data.
	set {theJson, theError} to JSONSerial's JSONObjectWithData:theData options:0 |error|:(reference)
	if theJson is missing value then error theError's localizedDescription() as text
	set MapQuestRoute to theJson's objectForKey:("route")
	set DistanceMilesNSNumber to MapQuestRoute's objectForKey:("distance")
	round (DistanceMilesNSNumber as real) rounding to nearest
end ParseJsonDistanceMilesIn:

To complete the scripting journey, I returned the numerical value in the following dialog


set MapQuestURLlString to my FinalDisplayMilesFromLocation:fromLocation toLocationSting:toLocation withDistanceMiles:DistanceMiles

on FinalDisplayMilesFromLocation:fromLocation toLocationSting:toLocation withDistanceMiles:DistanceMiles
	display dialog ((DistanceMiles as string) & "

Since I initially attempted to Applescript MapKit, I solved my prior Applescript error regarding assigning properties to an MKDirectionsRequest, by allocating a block of memory to the class to make it a pointer. After the allocation, my script succeeded at assigning a source, destination, transport type, and alternative route boolean. The following snippet changed from the script I previously posted above. I assigned the MapItemList to include two MapKit placemarks.

set directionsRequest to current application's MKDirectionsRequest's alloc()
	set sourceMKMapItem to (item 1 of MapItemList)
	directionsRequest's setSource:(item 1 of MapItemList)
	directionsRequest's setDestination:(item 2 of MapItemList)
	directionsRequest's setTransportType:MKDirectionsTransportTypeAutomobile
	directionsRequest's setRequestsAlternateRoutes:false

These lines of script produced the following successful MapKitDirectionsRequest:

Following that I was able to successfully allocate an instance of MKDirections and init it with the directionsRequest instance, using the following code:

set directions to current application's MKDirections's alloc()'s initWithRequest:directionsRequest

Having reached this point in the script, I have encountered a new problem, on which I need help. I cannot fathom a method to code an instance of MapKit MKDirections to calculate directions with a completion handler. I followed the Apple Developer method shown at https://developer.apple.com/documentation/mapkit/mkdirections/1452078-calculate

My following attempt to convert that Objective C code to Applescript failed.

set directions to current application's MKDirections's alloc()'s initWithRequest:directionsRequest
set response to current application's MKDirectionsResponse's alloc()'s init()
set carrot to current application's NSString's stringWithString:"^"
directions's calculateDirectionsWithCompletionHandler:carrot(response, |error|)

With the same goal of scripting MapKit to obtain the distance between two geographical locations or placemarks:

  1. How can I script this MapKit Objective C code successfully, to calculate MapKit’s directions?
  2. What is the best method to script the completion handler in this last line of code?

Unfortunately the short answer is that you can’t call methods like that using ASObjC. The completionHandler parameter takes what is called a block, which is a bit like an inline function. Blocks just aren’t available to ASObjC.

Sounds like a dead end to the Applescript MapKit endeavor, but thanks for your insight, Shane.

The Google Maps Directions API provides the driving distance along with other information between two addresses. The following handler, called drivingInfo, takes as input an AppleScript record with two properties:

 startingAddress - the starting address as a text string
 endingAddress - the ending address as a text string

It processes the input address information through the Directions API and returns an AppleScript record with the following output properties:

 startingLatitude - the latitude of the starting address
 startingLongitude - the longitude of the starting address
 endingLatitude - the latitude of the ending address
 endingLongitude - the longitude of the ending address
 drivingDurationInSeconds - the driving duration in seconds
 drivingDurationInHours - the driving duration in hours
 drivingDistanceInMeters - the driving distance in meters
 drivingDistanceInMiles - the driving distance in US statute miles
 stepByStepDirections - list of AppleScript records, each record containing directions and other information for that driving step

The handler works as follows:

 1) Creates the Directions API URL containing the starting and ending addresses
 2) Percent-encodes the URL so that special characters are handled properly
 3) Downloads the URL contents, which is a JSON string
 4) Converts the JSON string to an NSData object
 5) Converts the NSData object to a JSON object
 6) Converts the JSON object to an AppleScript record
 7) Extracts from the AppleScript record the desired property values

Handler:


use framework "Foundation"
use scripting additions

on drivingInfo({startingAddress:startingAddress, endingAddress:endingAddress})
	set urlString to "https://maps.googleapis.com/maps/api/directions/json?origin=" & startingAddress & "&destination=" & endingAddress
	set urlNSString to (current application's NSString's stringWithString:urlString)
	set urlAllowedCharacterSet to (current application's NSCharacterSet's URLQueryAllowedCharacterSet())
	set urlEncodedString to (urlNSString's stringByAddingPercentEncodingWithAllowedCharacters:urlAllowedCharacterSet)
	set urlObject to (current application's |NSURL|'s URLWithString:urlEncodedString)
	set {jsonString, usedEncoding} to (current application's NSString's stringWithContentsOfURL:urlObject usedEncoding:(reference) |error|:(missing value))
	set jsonNSString to (current application's NSString's stringWithString:jsonString)
	set jsonData to (jsonNSString's dataUsingEncoding:usedEncoding)
	set jsonObject to (current application's NSJSONSerialization's JSONObjectWithData:jsonData options:0 |error|:(missing value))
	set jsonRecord to jsonObject as record
	tell jsonRecord's routes's first item's legs's first item
		set startingLatitude to its start_location's lat
		set startingLongitude to its start_location's lng
		set endingLatitude to its end_location's lat
		set endingLongitude to its end_location's lng
		set drivingDurationInSeconds to its duration's value
		set drivingDurationInHours to (its duration's value) / 3600
		set drivingDistanceInMeters to its distance's value
		set drivingDistanceInMiles to (its distance's value) / 1609.3472
		set stepByStepDirections to its steps
	end tell
	return {startingLatitude:startingLatitude, startingLongitude:startingLongitude, endingLatitude:endingLatitude, endingLongitude:endingLongitude, drivingDurationInSeconds:drivingDurationInSeconds, drivingDurationInHours:drivingDurationInHours, drivingDistanceInMeters:drivingDistanceInMeters, drivingDistanceInMiles:drivingDistanceInMiles, stepByStepDirections:stepByStepDirections}
end drivingInfo

-- For example:

set addrStart to "Rockefeller Plaza, New York, NY 10111"
set addrEnd to "11 Wall St, New York, NY 10005"

drivingInfo({startingAddress:addrStart, endingAddress:addrEnd})
--> {
	startingLatitude:40.7590703, 
	startingLongitude:-73.9784577, 
	endingLatitude:40.7076638, 
	endingLongitude:-74.0101547, 
	drivingDurationInSeconds:1386, 
	drivingDurationInHours:0.385, 
	drivingDistanceInMeters:9452, 
	drivingDistanceInMiles:5.87318883085, 
	stepByStepDirections:{
		{
			html_instructions:"Head <b>southeast</b> on <b>W 50th St</b> toward <b>5th Ave</b>", 
			start_location:{
				lat:40.7590703, 
				lng:-73.9784577
			}, 
			distance:{
				value:958, 
				|text|:"0.6 mi"
			}, 
			travel_mode:"DRIVING", 
			duration:{
				value:343, 
				|text|:"6 mins"
			}, 
			polyline:{
				|points|:"ewwwFj|obMl@mBL_@DOf@_BRk@rBqGlBeGNg@Pg@~BoH`BgFRq@Lc@Ne@|CuJPg@"
			}, 
			end_location:{
				lat:40.7548795, 
				lng:-73.9685187
			}
		},
		[...record containing the driving information for the 2nd driving step...],
		[...record containing the driving information for the 3rd driving step...],
		...,
		[...record containing the driving information for the final driving step...]
	}

Nice handler, bmose. :cool:

A couple of minor points about these three lines:

set {jsonString, usedEncoding} to (current application's NSString's stringWithContentsOfURL:urlObject usedEncoding:(reference) |error|:(missing value))
set jsonNSString to (current application's NSString's stringWithString:jsonString)
set jsonData to (jsonNSString's dataUsingEncoding:usedEncoding)

jsonString is returned as an NSString anyway, so you could leave out the second line and have the first set jsonNSString directly:

set {jsonNSString, usedEncoding} to (current application's NSString's stringWithContentsOfURL:urlObject usedEncoding:(reference) |error|:(missing value))
set jsonData to (jsonNSString's dataUsingEncoding:usedEncoding)

But in fact the downloaded text can be packaged directly as NSData, with just one line:

set jsonData to (current application's NSData's dataWithContentsOfURL:urlObject)

Thank you. Not sure how I missed it.

Perfect! Here is the handler incorporating that streamlining suggestion:


on drivingInfo({startingAddress:startingAddress, endingAddress:endingAddress})
	set urlString to "https://maps.googleapis.com/maps/api/directions/json?origin=" & startingAddress & "&destination=" & endingAddress
	set urlNSString to (current application's NSString's stringWithString:urlString)
	set urlAllowedCharacterSet to (current application's NSCharacterSet's URLQueryAllowedCharacterSet())
	set urlEncodedString to (urlNSString's stringByAddingPercentEncodingWithAllowedCharacters:urlAllowedCharacterSet)
	set urlObject to (current application's |NSURL|'s URLWithString:urlEncodedString)
	set jsonData to (current application's NSData's dataWithContentsOfURL:urlObject)
	set jsonObject to (current application's NSJSONSerialization's JSONObjectWithData:jsonData options:0 |error|:(missing value))
	set jsonRecord to jsonObject as record
	tell jsonRecord's routes's first item's legs's first item
		set startingLatitude to its start_location's lat
		set startingLongitude to its start_location's lng
		set endingLatitude to its end_location's lat
		set endingLongitude to its end_location's lng
		set drivingDurationInSeconds to its duration's value
		set drivingDurationInHours to (its duration's value) / 3600
		set drivingDistanceInMeters to its distance's value
		set drivingDistanceInMiles to (its distance's value) / 1609.3472
		set stepByStepDirections to its steps
	end tell
	return {startingLatitude:startingLatitude, startingLongitude:startingLongitude, endingLatitude:endingLatitude, endingLongitude:endingLongitude, drivingDurationInSeconds:drivingDurationInSeconds, drivingDurationInHours:drivingDurationInHours, drivingDistanceInMeters:drivingDistanceInMeters, drivingDistanceInMiles:drivingDistanceInMiles, stepByStepDirections:stepByStepDirections}
end drivingInfo

Not having to use a MapQuest json API key allows your Google Maps json applescript greater universal use. Thank you all for the clear Applescript Objective C example.