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