Wednesday, December 13, 2017

#1 2017-09-05 09:56:59 am

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 219

Local time for any latitude/longitude location

I found that getting the local time for any arbitrary location programmatically was more challenging than it might seem at first glance.  The following handler takes as input any latitude and longitude location in decimal form and returns the current local time for that location as an Applescript date.

The handler effects its actions via a shell script that (1) gets raw timezone and daylight savings time data from the Google Maps API, (2) parses the result with the Python json module, and (3) converts the data to local time with Bash's date command.

The latitude and longitude values should be supplied to the handler as text strings rather than integer or real numbers to avoid the possibility of their being represented in exponential notation (which will fail) when converted to text by the handler.

Applescript:


on localTime(latitudeInDecimalForm, longitudeInDecimalForm)
   set {localMonth, localDay, localYear, localHour, localMinute, localSecond} to (do shell script "" & ¬
       "epochTime=$(date +%s)" & linefeed & ¬
       "x=$(curl -LNsf \"https://maps.googleapis.com/maps/api/timezone/json?location=" & latitudeInDecimalForm & "," & longitudeInDecimalForm & "&" & "timestamp=$epochTime\" | python -c '" & linefeed & ¬
       "import sys, json" & linefeed & ¬
       "y=json.load(sys.stdin)" & linefeed & ¬
       "print(y[\"dstOffset\"])" & linefeed & ¬
       "print(y[\"rawOffset\"])" & linefeed & ¬
       "sys.exit()" & linefeed & ¬
       "')" & linefeed & ¬
       "dstOffset=$(sed -En \"1p\" <<<\"$x\")" & linefeed & ¬
       "rawOffset=$(sed -En \"2p\" <<<\"$x\")" & linefeed & ¬
       "TZ=\"Etc/GMT\" date -j -r $(($epochTime + dstOffset + rawOffset)) \"+%m %d %Y %k %M %S\"")'s words
   copy (current date) to localDateTime
   tell localDateTime to set {its month, its day, its year, its hours, its minutes, its seconds} to {localMonth, localDay, localYear, localHour, localMinute, localSecond}
   return localDateTime
end localTime

Examples:

Applescript:


localTime("37.79735", "-122.465891") -- San Francisco, California: date "Tuesday, September 5, 2017 at 7:40:06 AM"
localTime("40.735662", "-73.989327") -- New York, New York: date "Tuesday, September 5, 2017 at 10:40:06 AM"
localTime("51.506767", "-0.12907") -- London, England: date "Tuesday, September 5, 2017 at 3:40:06 PM",
localTime("35.729599", "139.73346") -- Tokyo, Japan: date "Tuesday, September 5, 2017 at 11:40:06 PM"

Note:  In the curl command, "&" & "timestamp" is represented as two text strings to avoid potential html decoding problems when displayed by web browsers, but it can be condensed into a single text string after copying the code into Script Editor.

Last edited by bmose (2017-09-06 08:06:03 pm)


Filed under: date, Time, location, geocode

Offline

 

#2 2017-09-05 07:42:02 pm

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 5199

Re: Local time for any latitude/longitude location

FYI, here's an alternative approach using AppleScriptObjC:

Applescript:

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

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

Last edited by Shane Stanley (2017-09-06 06:16:45 pm)


Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/

Offline

 

#3 2017-09-05 09:59:08 pm

haolesurferdude
Member
Registered: 2010-02-19
Posts: 109

Re: Local time for any latitude/longitude location

Shane,

I copied your version of the routine and called it with:

localTime("37.79735", "-122.465891")

It compiles correctly, but when run the line that begins

set {theJson, theError} to (current application's

returns the following error

error "data parameter is nil" number -10000

with

JSONObjectWithData:theData options:0 |error|:(reference)

hilighted.

I'd love to have a working version of this.  Thanks in advance for your help.

Browser: Firefox 50.0
Operating System: Mac OS X (10.10)

Offline

 

#4 2017-09-06 12:24:37 am

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 5199

Re: Local time for any latitude/longitude location

The forum software automatically added url tags around the url string. I've split the string after "htt" to stop it happening.


Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/

Offline

 

#5 2017-09-06 07:20:47 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 4452

Re: Local time for any latitude/longitude location

I see the JSON object also has a timeZoneId property, which might be used instead of the two offsets. I don't know how it would affect performance, but it would simplify the code, especially in the shell script:

Applescript:


on localTime(latitudeInDecimalForm, longitudeInDecimalForm)
   set {localMonth, localDay, localYear, localHour, localMinute, localSecond} to (do shell script "" & ¬
       "epochTime=$(date +%s)" & linefeed & ¬
       "x=$(curl -LNsf \"https://maps.googleapis.com/maps/api/timezone/json?location=" & latitudeInDecimalForm & "," & longitudeInDecimalForm & "&" & "timestamp=$epochTime\" | python -c '" & linefeed & ¬
       "import sys, json" & linefeed & ¬
       "y=json.load(sys.stdin)" & linefeed & ¬
       "print(y[\"timeZoneId\"])" & linefeed & ¬
       "sys.exit()" & linefeed & ¬
       "')" & linefeed & ¬
       "TZ=$x date -jr $epochTime \"+%m %d %Y %k %M %S\"")'s words
   set localDateTime to (current date)
   tell localDateTime to set {its year, its day, its month, its day, its hours, its minutes, its seconds} to {localYear, 1, localMonth, localDay, localHour, localMinute, localSecond}
   return localDateTime
end localTime

In the ASObjC version, these three lines:

Applescript:

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)

… would be replaced with these two:

Applescript:

set timeZoneId to (theJson's objectForKey:"timeZoneId")
set timeZone to current application's NSTimeZone's timeZoneWithName:(timeZoneId)

Last edited by Nigel Garvey (2017-09-06 07:23:18 am)


NG

Offline

 

#6 2017-09-06 07:54:42 am

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 5199

Re: Local time for any latitude/longitude location

Nigel Garvey wrote:

I see the JSON object also has a timeZoneId property, which might be used instead of the two offsets.



I was a little nervous about how reliable that would be -- but I guess knownTimeZones() means known to the world rather than just some subset known to the OS. Here it returns 437 zones.


Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/

Offline

 

#7 2017-09-06 08:47:54 am

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 219

Re: Local time for any latitude/longitude location

Shane Stanley wrote:

FYI, here's an alternative approach using AppleScriptObjC:

Thanks for the AppleScriptObjC solution. I had a feeling one might be coming!

Nigel Garvey wrote:

I see the JSON object also has a timeZoneId property, which might be used instead of the two offsets.

That's a nice way to pare down the code a bit. Another would be to combine all the shell activities into a single Python script (I kept the two offsets since they have been reduced to a single line of code anyway, although the timeZoneId approach likely could be incorporated into the Python solution):

Applescript:


on localTime(latitudeInDecimalForm, longitudeInDecimalForm)
   set {localMonth, localDay, localYear, localHour, localMinute, localSecond} to (do shell script "" & ¬
       "python -c '" & linefeed & ¬
       "import urllib, json, time" & linefeed & ¬
       "epochTime=int(time.time())" & linefeed & ¬
       "x = json.loads(urllib.urlopen(\"https://maps.googleapis.com/maps/api/timezone/json?location=\"+(\"%.9f\" % " & latitudeInDecimalForm & ")+\",\"+(\"%.9f\" % " & longitudeInDecimalForm & ")+\"&" & "timestamp=\"+str(epochTime)).read())" & linefeed & ¬
       "print(time.strftime(\"%m %d %Y %k %M %S\", time.gmtime(epochTime+int(x[\"dstOffset\"])+int(x[\"rawOffset\"]))))" & linefeed & ¬
       "'")'s words
   tell (current date) to set {localDateTime, its day, its year, its month, its day, its hours, its minutes, its seconds} to {it, 1, localYear, localMonth, localDay, localHour, localMinute, localSecond}
   return localDateTime
end localTime

As an aside, can you kindly explain how the double assignment to its day works in the assignment statement:

Applescript:

tell localDateTime to set {its year, its day, its month, its day, its hours, its minutes, its seconds} to {localYear, 1, localMonth, localDay, localHour, localMinute, localSecond}

I don't understand that. Thanks.

Edit notes:
- This version contains improvements over the shell script solution initially submitted in this thread and is recommended over the initial version.
- Shane Stanley's efficient one-line date assignment algorithm was incorporated into the current version of the script.
- Also, the input parameters are now formatted by the Python script to handle numbers in exponential notation.  Thus, they can be entered as numbers (or text strings if desired) without fear of breaking the script (including correction of a previously erroneous formatting specification to its now correct version: \"%.9f\").

Last edited by bmose (2017-09-07 08:07:05 pm)

Offline

 

#8 2017-09-06 10:20:37 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 4452

Re: Local time for any latitude/longitude location

bmose wrote:

As an aside, can you kindly explain how the double assignment to its day works in the assignment statement:

Applescript:

tell localDateTime to set {its year, its day, its month, its day, its hours, its minutes, its seconds} to {localYear, 1, localMonth, localDay, localHour, localMinute, localSecond}

I don't understand that. Thanks.


Hi. Yes.

AppleScript dates overflow into the following month if you set a day which doesn't exist in their current month or if you set a month which doesn't have their current day. For instance, supposing you ran your script in the US late in the evening on 31st March to find the local time in Japan. The date there by then would be 1st April. Setting the month of your current date to April would give 31st April, which, since April only has 30 days, would roll over to 1st May. Setting the day to 1 first (or anything up to 28) before setting the month ensures that there's no overflow, whatever the month. Then once the month's set, you can safely set the day to a day that you know is in it.

Last edited by Nigel Garvey (2017-09-06 10:21:57 am)


NG

Offline

 

#9 2017-09-06 10:28:11 am

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 219

Re: Local time for any latitude/longitude location

Nigel Garvey wrote:

AppleScript dates overflow...


Awesome explanation. Thanks!

Offline

 

#10 2017-09-06 11:19:21 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 4452

Re: Local time for any latitude/longitude location

I wrote:

Setting the day to 1 first (or anything up to 28) before setting the month ensures that there's no overflow, whatever the month. Then once the month's set, you can safely set the day to a day that you know is in it.



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.


NG

Offline

 

#11 2017-09-06 11:38:20 am

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 219

Re: Local time for any latitude/longitude location

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

Offline

 

#12 2017-09-06 06:19:34 pm

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 5199

Re: Local time for any latitude/longitude location

bmose wrote:

Shane Stanley's efficient one-line date assignment algorithm was incorporated into the current version of the script.



Actually, that line was originally written by Nigel elsewhere.

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


Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/

Offline

 

#13 2017-09-07 01:17:30 am

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 5199

Re: Local time for any latitude/longitude location

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

Applescript:

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:

Applescript:

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

Last edited by Shane Stanley (2017-09-08 07:41:33 pm)


Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/

Offline

 

#14 2017-09-07 05:59:17 am

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 219

Re: Local time for any latitude/longitude location

Shane Stanley wrote:

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

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.

Last edited by bmose (2017-09-07 07:04:32 am)

Offline

 

#15 2017-09-07 08:55:11 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 4452

Re: Local time for any latitude/longitude location

Nice scripts, both of you.  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!)  smile


NG

Offline

 

#16 2017-09-07 06:26:02 pm

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 5199

Re: Local time for any latitude/longitude location

Nigel Garvey wrote:

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.



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


Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/

Offline

 

#17 2017-09-07 11:07:31 pm

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 219

Re: Local time for any latitude/longitude location

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 :-) . 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:

Applescript:


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:

Applescript:


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.

Last edited by bmose (2017-09-08 09:38:26 am)

Offline

 

#18 2017-09-08 06:23:55 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 4452

Re: Local time for any latitude/longitude location

I wrote:

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.


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

Applescript:

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".

bmose wrote:

Applescript:

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"}


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


NG

Offline

 

#19 2017-09-08 08:19:37 am

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 5199

Re: Local time for any latitude/longitude location

Nigel Garvey wrote:

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… smile


Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/

Offline

 

#20 2017-09-08 08:45:27 am

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 219

Re: Local time for any latitude/longitude location

Nigel Garvey wrote:

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.

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.

Last edited by bmose (2017-09-08 09:07:04 am)

Offline

 

#21 2017-09-08 09:26:47 am

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 219

Re: 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.)

Applescript:


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"}

Offline

 

#22 2017-09-10 04:02:35 pm

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 219

Re: Local time for any latitude/longitude location

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:

Applescript:

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:

Applescript:

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:

Applescript:

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:

Applescript:

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

Last edited by bmose (2017-11-20 03:03:56 pm)

Offline

 

#23 2017-09-19 02:09:29 am

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 5199

Re: Local time for any latitude/longitude location

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

Applescript:

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:

Applescript:

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)


Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/

Offline

 

#24 2017-09-19 05:08:59 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 4452

Re: Local time for any latitude/longitude location

Shane Stanley wrote:

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


Cool.  cool

Applescript:

   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()


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?


NG

Offline

 

#25 2017-09-19 05:59:42 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 4452

Re: Local time for any latitude/longitude location

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:

Applescript:

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:


NG

Offline

 

Board footer

Powered by FluxBB

RSS (new topics) RSS (active topics)