Local time for any latitude/longitude location

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.