Local time for any latitude/longitude location

Awesome explanation. Thanks!

I should add that it’s also a good idea to set the year before setting the month and the day to ensure that you don’t get overflow when setting 29th February. It’s not actually an issue here as your script’s never going to set 29th February from a different year, but for the general technique of changing the values of AppleScript dates, it’s worth remembering.

Thanks again. I made the change to the “improved” version of the shell script solution above.

Actually, that line was originally written by Nigel elsewhere.

I’ve also edited my version to include a check for the JSON status value.

Here’s a companion script for getting the coordinates from an address:

use AppleScript version "2.4" --  (10.10) or later
use framework "Foundation"
use scripting additions

on coordsForAddress: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 coordsForAddress:

Which means you can combine them like this:

set {latitudeInDecimalForm, longitudeInDecimalForm, fullAddress} to my coordsForAddress:"Akershus Fortress"
set theTime to my localTime(latitudeInDecimalForm, longitudeInDecimalForm)
display dialog "At " & fullAddress & " it's " & (theTime as text)

Updated with info from message #11

Very nice addition.

One slight word of caution. From some random samplings, I found that the Google Maps API will sometimes return the adjacent timezone, not the actual timezone, within a few hundred yards/meters (or even a few miles/kilometers in some cases) of a timezone border. It’s obviously a limitation of the Google Maps API and not of the algorithms presented above. This should rarely be a problem in real-world usage but something to be aware of if the geocode location is extremely close to a timezone border.

Nice scripts, both of you. :slight_smile:

I’ve been fooling around with Shane’s coordinate-finding script this morning to see if the JSON’s results array actually contains more than one result when the input’s deliberately vague. The answer so far is that it doesn’t. With an input of “Stratford”, the array only contains information for Stratford, CT, USA. Nothing for Stratford, Ontario, Canada or any of the Stratfords in England. “St. Petersburg” only gets the Russian city, not the one in Florida. Similarly arbitrary (but reasonable) results are obtained with “Perth”, “Richmond”, “Paris”, and “Aldershot”. More specific input does get the required information, but the array only ever contains one result.

Entering just the name of my village returns details for a numbered building in W Pico Blvd, Los Angeles, California! (Googling the address turns up an establishment of unstated function, but which is open in the evenings and has a library, a lounge, a games room, a bar, and my village’s name!) :slight_smile:

FWIW, I saw the same thing. I half-wondered if using an API key would make a difference.

The additional functionality provided by Shane Stanley’s companion script inspired the following script, which combines all the previously described functionality into a single handler localGeoInfo.

The handler’s input argument is a record that may be coded with lat and lng properties specifying the location’s latitude and longitude in decimal form (as integers, real numbers, exponential numbers, or text representations of the same), or with an addr property specifying the location’s street or similar address as a text string. Error checking is performed on the input argument and the processing of the Google Maps API data.

The handler’s return value is a record with lat, lng, addr, and localtime properties. The first two properties are the input location’s latitude and longitude as real numbers, the next is the location’s address as a text string (as "best-guess"ed by the Google Maps API), and the last is the location’s local time as an Applescript date value. Note in the examples below the slight tweaking of the output location during the processing of the input location by the Google Maps API.

In keeping with my prior entries, the handler is a shell script solution that utilizes Python for downloading and processing of the url data from the Google Maps API, although it could of course be coded in AppleScriptObjC :slight_smile: . Since Python relies on indentation for script parsing, it is important to preserve the leading tabs in the indented Python lines when copying the code.

Examples:


localGeoInfo({lat:40.7593755, lng:-73.9799726}) --> {lat:40.7593755, lng:-73.9799726, addr:"Rockefeller Plaza, 45 Rockefeller Plaza, New York, NY 10111, USA", localtime:date "Thursday, September 7, 2017 at 11:58:20 PM"}

localGeoInfo({addr:"30 Rockefeller Plaza, New York, NY 10112, USA"}) --> {lat:40.7593755, lng:-73.9799726, addr:"Rockefeller Plaza, 45 Rockefeller Plaza, New York, NY 10111, USA", localtime:date "Thursday, September 7, 2017 at 11:58:20 PM"}

Handler:


on localGeoInfo(inputRecord)
	try
		if inputRecord's class ≠ record then error number 999
		tell (inputRecord & {lat:null, lng:null, addr:null}) to set {lat, lng, addr} to {its lat, its lng, its addr}
		try
			{lat as number, lng as number}
			set isLatLng to true
		on error
			if addr's class ≠ text then error number 999
			set isLatLng to false
		end try
		tell (do shell script "" & ¬
			"python -c '" & linefeed & ¬
			"import json, string, time, urllib" & linefeed & ¬
			"epochTime=int(time.time())" & linefeed & ¬
			"isLatLng = " & (isLatLng as integer) & " == 1" & linefeed & ¬
			"theLat = theLng = theAddr = theTime = \"\"" & linefeed & ¬
			"try:" & linefeed & ¬
			tab & "if isLatLng:" & linefeed & ¬
			tab & tab & "theUrl = \"https://maps.googleapis.com/maps/api/geocode/json?latlng=\" + (\"%.9f\" % " & lat & ") + \",\" + (\"%.9f\" % " & lng & ")" & linefeed & ¬
			tab & "else:" & linefeed & ¬
			tab & tab & "theUrl = \"https://maps.googleapis.com/maps/api/geocode/json?address=\" + string.replace(\"" & addr & "\", \" \", \"+\")" & linefeed & ¬
			tab & "x = json.loads(urllib.urlopen(theUrl).read())[\"results\"]" & linefeed & ¬
			tab & "y = x[0]" & linefeed & ¬
			tab & "for z in x:" & linefeed & ¬
			tab & tab & "if \"street_address\" in set(z[\"types\"]):" & linefeed & ¬
			tab & tab & tab & "y = z" & linefeed & ¬
			tab & tab & tab & "break" & linefeed & ¬
			tab & "theLat = y[\"geometry\"][\"location\"][\"lat\"]" & linefeed & ¬
			tab & "theLng = y[\"geometry\"][\"location\"][\"lng\"]" & linefeed & ¬
			tab & "theAddr = y[\"formatted_address\"]" & linefeed & ¬
			tab & "theUrl = \"https://maps.googleapis.com/maps/api/timezone/json?location=\" + str(theLat) + \",\" + str(theLng) + \"&\" + \"timestamp=\" + str(epochTime)" & linefeed & ¬
			tab & "x = json.loads(urllib.urlopen(theUrl).read())" & linefeed & ¬
			tab & "theTime = time.strftime(\"%Y %m %d %k %M %S\", time.gmtime(epochTime+int(x[\"dstOffset\"])+int(x[\"rawOffset\"])))" & linefeed & ¬
			"except: pass" & linefeed & ¬
			"if theLat == theLng == theAddr == theTime == \"\":" & linefeed & ¬
			tab & "if isLatLng: raise Exception(\"Could not get location information for the input latitude and longitude.\")" & linefeed & ¬
			tab & "else: raise Exception(\"Could not get location information for the input address.\")" & linefeed & ¬
			"print(theLat)" & linefeed & ¬
			"print(theLng)" & linefeed & ¬
			"print(theAddr).encode(\"utf-8\")" & linefeed & ¬
			"print(theTime)" & linefeed & ¬
			"'")'s paragraphs to set {theLat, theLng, theAddr, {theYear, theMonth, theDay, theHour, theMinute, theSecond}} to {(item 1) as real, (item 2) as real, item 3, item 4's words}
		tell (current date) to set {theTime, its day, its year, its month, its day, its hours, its minutes, its seconds} to {it, 1, theYear, theMonth, theDay, theHour, theMinute, theSecond}
	on error m number n
		if n = 999 then error "The input argument is invalid." & return & return & "It must be a record in either of the following forms:" & return & return & tab & "{lat:...latitude in decimal form..., lng:...longitude in decimal form...}" & return & tab & tab & "-OR-" & return & tab & "{addr:...address as a text string...}"
		set o to offset of "Exception: " in m
		if o > 0 then error (get m's text (o + 11) thru -1)
		error m number n
	end try
	return {lat:theLat, lng:theLng, addr:theAddr, localtime:theTime}
end localGeoInfo

Edit notes:

  • The script has been modified from that originally submitted. Previously, the latitude, longitude, and address location data was obtained from the first entry of the JSON “results” array. Now, the “results” array is searched in a for loop for an entry whose “types” array includes a “street_address” entry. If one is found, that “results” entry is used for the location data; otherwise, the first “results” array entry is used. This modification should improve the accuracy of the returned location data in certain cases of multiple conflicting location entries.
  • The Python print(theAddr) statement was modified to handle non-ASCII characters in addresses properly.

That said, the results array in the JSON from a geocode query with bmose’s coordinates …

localGeoInfo({lat:40.7593755, lng:-73.9799726}) --> {lat:40.7593755, lng:-73.9799726, addr:"Rockefeller Plaza, 45 Rockefeller Plaza, New York, NY 10111, USA", localtime:date "Thursday, September 7, 2017 at 11:58:20 PM"}

… contains ten results, for: “Rockefeller Plaza, 45 Rockefeller Plaza, New York, NY 10111, USA”, “60 W 50th St, New York, NY 10112, USA”, “GE Building, New York, NY 10112, USA”, “Midtown West, New York, NY, USA”, “Midtown, New York, NY, USA”, “Manhattan, New York, NY, USA”, “New York, NY, USA”, “New York, NY 10112, USA”, “New York County, New York, NY, USA”, and “New York-Northern New Jersey-Long Island, NY-NJ-PA, USA”.

I confirm that when you ask about 30 Rockefeller Plaza, NY 10112, maps.googleapis.com returns information about 45 Rockefeller Plaza, NY 10111.

When I try it here I get two results. The first is 45 Rockefeller Plaza, and the second is 30 Rockefeller Plaza. But the full address for the first is “Rockefeller Plaza, 45 Rockefeller Plaza, New York, NY 10111, USA”, as opposed to “30 Rockefeller Plaza, New York, NY 10112, USA”, so I’m presuming the repeated Rockefeller Plaza is behind the odd ranking.

Wouldn’t want to use it to call a taxi… :slight_smile:

I too have been a bit befuddled by the common occurrence of numerous location entries for a given JSON “results” array. In general, I’ve found that the “results” array entry whose “types” array’s first entry is “street_address” tends to be the address most in line with the address I expect to find. However, after encountering a few “results” arrays without any “street_address” entry, I initially decided to use the first “results” array entry for all queries, since the locations seem to move from the most localized to the most general the farther along the “results” array one goes. Then I tried a street address which I knew would be ambiguous, namely 100 Washington St, Boston, Massachusetts, because there are multiple such streets in Boston’s subdivisions. Once again, the “results” array entry whose “types” array’s first entry is “street_address” was the most “expected” address, i.e., the address in the center of Boston rather than in one of its subdivisions. However, it was not the first “results” array entry! Therefore, I have modified the previously submitted localGeoInfo handler such that it now searches the “results” array for a location entry whose “types” array’s first entry is “street_address”. If one is found, it uses that location; otherwise, it simply uses the “results” array’s first entry.

Addendum: Actually, I modified the code such that “street_address” does not have to be the first “types” array entry but rather may appear anywhere in the “types” array for that location to be selected.

After modifying the localGeoInfo handler to use preferentially the location data marked by a “types” array containing a “street_address” entry, lo and behold, the returned result is cleaner. No guarantees that this will be a universal finding (I’m sure it won’t), but it seems to be a step in the right direction. (See the original localGeoInfo post for the handler code.)


localGeoInfo({addr:"30 Rockefeller Plaza, New York, NY 10112, USA"}) --> {lat:40.7589632, lng:-73.9793374, addr:"30 Rockefeller Plaza, New York, NY 10112, USA", localtime:date "Friday, September 8, 2017 at 10:20:49 AM"}

localGeoInfo({lat:40.7589632, lng:-73.9793374}) --> {lat:40.7589632, lng:-73.9793374, addr:"30 Rockefeller Plaza, New York, NY 10112, USA", localtime:date "Friday, September 8, 2017 at 10:20:49 AM"}

Edit note 20 Nov 2017: An AppleScriptObjC version has been added. It is the recommended version given that Cocoa’s NSJSONSerialization class is preferred for JSON parsing over the Applescript/sed approach (as discussed elsewhere in this thread) and is faster than the Python solution (which requires a do shell script invocation).

Usage:
use framework “Foundation”
use scripting additions

localGeoInfo({lat:…input location’s latitude…, lng:…input location’s longitude…})
-or-
localGeoInfo({addr:…input location’s address…})

Returned record for the input location:
{lat:…latitude…, lng:…longitude…, addr:…address…, localTime:…local time…}

········································································································································································

Not to prolong this thread unnecessarily, but for completeness, here is a version of the localGeoInfo handler functionally identical to the previously submitted Python version except that the JSON data processing is performed in Applescript rather than Python.

The Applescript version avoids the extensive metaprogramming of the Python version. It is facilitated by the remarkable similarity of the Applescript and JSON value specifications, the primary differences being JSON’s use of double-quoted text strings for record labels (called object keys in JSON) and JSON’s allowance of a single value to span multiple lines. JSON’s use of square brackets to enclose lists (called arrays in JSON) does not require transformation, since square brackets are an acceptable alternative to curly braces for enclosing Applescript lists. Thus, JSON-to-Applescript decoding can be accomplished with the following compound command, which encloses in pipes any record labels that require them:

set applescriptValue to run script (do shell script ("echo " & jsonValue's quoted form & " | sed -E 's/\"([^\"]+)\"[[:space:]]*:[[:space:]]*/|\\1|:/g;' | tr -d '\\n'"))

Here is the Applescript version of the localGeoInfo handler functionally identical to the Python version. It utilizes the above command for JSON decoding and Bash for reformatting certain strings:

on localGeoInfo(inputRecord)
	-- Applescript version
	try
		if inputRecord's class ≠ record then error
		tell (inputRecord & {lat:null, lng:null, addr:null}) to set {lat, lng, addr} to {its lat, its lng, its addr}
		try
			set {isLatLng, {lat, lng}} to {true, (do shell script ("printf \"%.9f\\n%.9f\" " & lat & " " & lng))'s paragraphs}
		on error
			if addr's class ≠ text then error
			set {isLatLng, addr} to {false, do shell script ("tr \" \" \"+\" <<<\"" & addr & "\"")}
		end try
	on error
		error "The input argument is invalid." & return & return & "It must be a record in either of the following forms:" & return & return & tab & "{lat:...latitude in decimal form..., lng:...longitude in decimal form...}" & return & tab & tab & "-OR-" & return & tab & "{addr:...address as a text string...}"
	end try
	set {epochDate, epochTime} to {(current date) - (time to GMT), do shell script "date \"+%s\""}
	set {theLat, theLng, theAddr, theTime} to {"", "", "", ""}
	try
		if isLatLng then
			set theUrl to "h" & "ttps://maps.googleapis.com/maps/api/geocode/json?latlng=" & lat & "," & lng
		else
			set theUrl to "h" & "ttps://maps.googleapis.com/maps/api/geocode/json?address=" & addr
		end if
		set jsonRecord to run script (do shell script ("curl -LNsf " & theUrl's quoted form & " | sed -E 's/\"([^\"]+)\"[[:space:]]*:[[:space:]]*/|\\1|:/g;' | tr -d '\\n'"))
		tell jsonRecord's results
			tell first item to set {theLat, theLng, theAddr} to {its geometry's location's lat, its geometry's location's lng, its formatted_address}
			repeat with i in it
				tell i
					if its |types| contains "street_address" then
						set {theLat, theLng, theAddr} to {its geometry's location's lat, its geometry's location's lng, its formatted_address}
						exit repeat
					end if
				end tell
			end repeat
		end tell
	on error m number n
		error "Could not get the input location's latitude/longitude coordinates and/or address." & return & return & "(" & n & "): " & m
	end try
	try
		set theUrl to "h" & "ttps://maps.googleapis.com/maps/api/timezone/json?location=" & theLat & "," & theLng & "&" & "timestamp=" & epochTime
		set jsonRecord to run script (do shell script ("curl -LNsf " & theUrl's quoted form & " | sed -E 's/\"([^\"]+)\"[[:space:]]*:[[:space:]]*/|\\1|:/g;' | tr -d '\\n'"))
		tell jsonRecord to set theTime to epochDate + (its dstOffset) + (its rawOffset)
	on error m number n
		error "Could not get the input location's local time." & return & return & "(" & n & "): " & m
	end try
	return {lat:theLat, lng:theLng, addr:theAddr, localtime:theTime}
end localGeoInfo

And here is the previously submitted Python version with date handling tidied up a bit:

on localGeoInfo(inputRecord)
	-- Python version
	try
		if inputRecord's class ≠ record then error number 999
		tell (inputRecord & {lat:null, lng:null, addr:null}) to set {lat, lng, addr} to {its lat, its lng, its addr}
		try
			{lat as number, lng as number}
			set isLatLng to true
		on error
			if addr's class ≠ text then error number 999
			set isLatLng to false
		end try
		set epochDate to (current date) - (time to GMT)
		tell (do shell script "" & ¬
			"python -c '" & linefeed & ¬
			"import json, string, time, urllib" & linefeed & ¬
			"epochTime=int(time.time())" & linefeed & ¬
			"isLatLng = " & (isLatLng as integer) & " == 1" & linefeed & ¬
			"theLat = theLng = theAddr = theTime = \"\"" & linefeed & ¬
			"try:" & linefeed & ¬
			tab & "if isLatLng:" & linefeed & ¬
			tab & tab & "theUrl = \"https://maps.googleapis.com/maps/api/geocode/json?latlng=\" + (\"%.9f\" % " & lat & ") + \",\" + (\"%.9f\" % " & lng & ")" & linefeed & ¬
			tab & "else:" & linefeed & ¬
			tab & tab & "theUrl = \"https://maps.googleapis.com/maps/api/geocode/json?address=\" + string.replace(\"" & addr & "\", \" \", \"+\")" & linefeed & ¬
			tab & "x = json.loads(urllib.urlopen(theUrl).read())[\"results\"]" & linefeed & ¬
			tab & "y = x[0]" & linefeed & ¬
			tab & "for z in x:" & linefeed & ¬
			tab & tab & "if \"street_address\" in set(z[\"types\"]):" & linefeed & ¬
			tab & tab & tab & "y = z" & linefeed & ¬
			tab & tab & tab & "break" & linefeed & ¬
			tab & "theLat = y[\"geometry\"][\"location\"][\"lat\"]" & linefeed & ¬
			tab & "theLng = y[\"geometry\"][\"location\"][\"lng\"]" & linefeed & ¬
			tab & "theAddr = y[\"formatted_address\"]" & linefeed & ¬
			tab & "theUrl = \"https://maps.googleapis.com/maps/api/timezone/json?location=\" + str(theLat) + \",\" + str(theLng) + \"&\" + \"timestamp=\" + str(epochTime)" & linefeed & ¬
			tab & "x = json.loads(urllib.urlopen(theUrl).read())" & linefeed & ¬
			tab & "dstOffset = int(x[\"dstOffset\"])" & linefeed & ¬
			tab & "rawOffset = int(x[\"rawOffset\"])" & linefeed & ¬
			"except: pass" & linefeed & ¬
			"if theLat == theLng == theAddr == theTime == \"\":" & linefeed & ¬
			tab & "if isLatLng: raise Exception(\"Could not get location information for the input latitude and longitude.\")" & linefeed & ¬
			tab & "else: raise Exception(\"Could not get location information for the input address.\")" & linefeed & ¬
			"print(theLat)" & linefeed & ¬
			"print(theLng)" & linefeed & ¬
			"print(theAddr).encode(\"utf-8\")" & linefeed & ¬
			"print(dstOffset)" & linefeed & ¬
			"print(rawOffset)" & linefeed & ¬
			"'")'s paragraphs to set {theLat, theLng, theAddr, theTime} to {(item 1) as real, (item 2) as real, item 3, epochDate + (item 4) + (item 5)}
	on error m number n
		if n = 999 then error "The input argument is invalid." & return & return & "It must be a record in either of the following forms:" & return & return & tab & "{lat:...latitude in decimal form..., lng:...longitude in decimal form...}" & return & tab & tab & "-OR-" & return & tab & "{addr:...address as a text string...}"
		set o to offset of "Exception: " in m
		if o > 0 then error (get m's text (o + 11) thru -1)
		error m number n
	end try
	return {lat:theLat, lng:theLng, addr:theAddr, localtime:theTime}
end localGeoInfo

Here is the AppleScriptObjC (recommended) solution:

use framework "Foundation"
use scripting additions

on localGeoInfo(inputRecord)
	-- AppleScriptObjC version
	try
		if inputRecord's class ≠ record then error
		tell (inputRecord & {lat:null, lng:null, addr:null}) to set {lat, lng, addr} to {its lat, its lng, its addr}
		try
			set {isLatLng, {lat, lng}} to {true, (do shell script ("printf \"%.9f\\n%.9f\" " & lat & " " & lng))'s paragraphs}
		on error
			if addr's class ≠ text then error
			set {isLatLng, addr} to {false, do shell script ("tr \" \" \"+\" <<<\"" & addr & "\"")}
		end try
	on error
		error "The input argument is invalid." & return & return & "It must be a record in either of the following forms:" & return & return & tab & "{lat:...latitude in decimal form..., lng:...longitude in decimal form...}" & return & tab & tab & "-OR-" & return & tab & "{addr:...address as a text string...}"
	end try
	set {epochDate, epochTime} to {(current date) - (time to GMT), do shell script "date \"+%s\""}
	set {theLat, theLng, theAddr, theTime} to {"", "", "", ""}
	try
		if isLatLng then
			set theUrl to "h" & "ttps://maps.googleapis.com/maps/api/geocode/json?latlng=" & lat & "," & lng
		else
			set theUrl to "h" & "ttps://maps.googleapis.com/maps/api/geocode/json?address=" & addr
		end if
		set jsonRecord to my parseJson(theUrl)
		tell jsonRecord's results
			tell first item to set {theLat, theLng, theAddr} to {its geometry's location's lat, its geometry's location's lng, its formatted_address}
			repeat with i in it
				tell i
					if its types contains "street_address" then
						set {theLat, theLng, theAddr} to {its geometry's location's lat, its geometry's location's lng, its formatted_address}
						exit repeat
					end if
				end tell
			end repeat
		end tell
	on error m number n
		error "Could not get the input location's latitude/longitude coordinates and/or address." & return & return & "(" & n & "): " & m
	end try
	try
		set theUrl to "h" & "ttps://maps.googleapis.com/maps/api/timezone/json?location=" & theLat & "," & theLng & "&" & "timestamp=" & epochTime
		set jsonRecord to my parseJson(theUrl)
		tell jsonRecord to set theTime to epochDate + (its dstOffset) + (its rawOffset)
	on error m number n
		error "Could not get the input location's local time." & return & return & "(" & n & "): " & m
	end try
	return {lat:theLat, lng:theLng, addr:theAddr, localtime:theTime}
end localGeoInfo

on parseJson(urlString)
	set urlObj to ((current application's |NSURL|)'s URLWithString:urlString)
	set {jsonString, usedEncoding} to ((current application's NSString)'s stringWithContentsOfURL:urlObj usedEncoding:(reference) |error|:(missing value))
	set dataObj to (jsonString's dataUsingEncoding:usedEncoding)
	set jsonRecord to ((current application's NSJSONSerialization)'s JSONObjectWithData:dataObj options:0 |error|:(missing value)) as record
	return jsonRecord
end parseJson

And here’s a handler for displaying the address in Maps.app:

use AppleScript version "2.4" --  (10.10) or later
use framework "Foundation"
use framework "CoreLocation"
use framework "MapKit"
use scripting additions

on showAddress:fullAddress latitude:theLat longitude:theLong
	-- get address components
	set fullAddress to current application's NSString's stringWithString:fullAddress
	set theDD to current application's NSDataDetector's dataDetectorWithTypes:(current application's NSTextCheckingTypeAddress) |error|:(missing value)
	set theMatch to theDD's firstMatchInString:fullAddress options:0 range:{0, fullAddress's |length|()}
	if theMatch = missing value then error "Can't interpret address"
	set addressDict to theMatch'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:addressDict)
	-- create a map item using the placemark
	set aMapItem to (current application's MKMapItem's alloc()'s initWithPlacemark:aPlacemark)
	-- create a dictionary of the map settings
	set launchDict to current application's NSDictionary's dictionaryWithObject:(get current application's MKMapTypeStandard) forKey:(current application's MKLaunchOptionsMapTypeKey)
	-- display the location in the Maps application
	aMapItem's openInMapsWithLaunchOptions:launchDict
end showAddress:latitude:longitude:

So a script also using the previous handlers might be like this:

use AppleScript version "2.4" --  (10.10) or later
use framework "Foundation"
use framework "CoreLocation"
use framework "MapKit"
use scripting additions

set addressString to "30 Rockefeller Plaza, New York"
set {theLat, theLong, fullAddress} to my coordsForAddress:addressString
set theTime to my localTime(theLat, theLong)
--display dialog "At " & fullAddress & " it's " & (theTime as text)
my showAddress:fullAddress latitude:theLat longitude:theLong
display notification fullAddress with title "Search result" subtitle (theTime as text)

Cool. :cool:

I see in the Xcode documentation that an NSTextCheckingResult may have both a components property and an addressComponents property. The latter’s specifically for address results; the former looks more general in its description on the main NSTextCheckingResult page, but is described on its individual page as “Currently used by the transit checking result.” Both properties work here and return identical dictionaries. Is there any particular advantage to either?

By the way, here’s Shane’s coordsForAddress: handler (post #13) with a couple of lines added to percent-encode any diacriticals in the address for the URL query:

use AppleScript version "2.4" -- (10.10) or later
use framework "Foundation"

on coordsForAddress:addressString
	set addressString to current application's NSString's stringWithString:addressString
	set addressString to addressString's stringByReplacingOccurrencesOfString:space withString:"+"
	set URLQueryAllowedChrs to current application's NSCharacterSet's URLQueryAllowedCharacterSet() -- Added.
	set addressString to addressString's stringByAddingPercentEncodingWithAllowedCharacters:URLQueryAllowedChrs -- Added.
	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 coordsForAddress:

I’m not sure why I used components, to be honest – I’m pretty sure I originally used addressComponents, which I saw in the docs, but must have changed it later (that code was removed and reinstated a couple of times).

However I just looked up the header file and the entry for addressComponents has the comment: Deprecated in favor of components, so I’ll put it down to subliminal something-or-other :slight_smile:

Thanks!

Hi, developers.

As I understand it, the service used in ASObjC variants to get latitude/longitude from the specified address is paid. Is there any unpaid service? I am not a developer, but an ordinary amateur.

And why do shell script variant (curl) for local time works for me without any problem? Without having any authentication key?

in ASObjC variant for getting local time I get error “REQUEST DENIED” on this line:

if not (theStatus's isEqualToString:"OK") then error theStatus as text

I tried this:

use AppleScript version "2.4" --  (10.10) or later
use framework "Foundation"
use framework "CoreLocation"
use framework "MapKit"
use scripting additions

set {theLat, theLong} to {"37.79735", "-122.465891"}
set theTime to my localTime(theLat, theLong)

on localTime(latitudeInDecimalForm, longitudeInDecimalForm)
	set theDate to current application's NSDate's |date|()
	set timeSeconds to theDate's timeIntervalSince1970()
	set nf to current application's NSNumberFormatter's new()
	nf's setMaximumFractionDigits:0
	set timeStamp to nf's stringFromNumber:timeSeconds
	set urlString to "htt" & "ps://maps.googleapis.com/maps/api/timezone/json?location=" & latitudeInDecimalForm & "," & longitudeInDecimalForm & "&" & "timestamp=" & timeStamp
	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 dstOffset to (theJson's objectForKey:"dstOffset") as integer
	set rawOffset to (theJson's objectForKey:"rawOffset") as integer
	set timeZone to current application's NSTimeZone's timeZoneForSecondsFromGMT:(dstOffset + rawOffset)
	set theCalendar to current application's NSCalendar's currentCalendar()
	set comps to theCalendar's componentsInTimeZone:timeZone fromDate:theDate
	tell (current date) to set {theASDate, year, day, its month, day, time} to ¬
		{it, comps's |year|(), 1, comps's |month|(), comps's |day|(), (comps's hour()) * hours + (comps's minute()) * minutes + (comps's |second|())}
	return theASDate
end localTime

Hi.

The requirement for an authentication key appears to have been introduced sometime in the past couple of years. The scripts worked when we wrote them. :frowning:

The options for the “curl” command in that script make it fail silently in the case of an error. The “local time” the script returns on my machine is GMT. So it looks as though the non-existent “offsets” being added to the GMT date in the last line of the shell script are both being treated as zero.