Return specific values fromJSON Location Helper File (google geocode)

Hello,

I am working on a script that receives a file from Hazel, determines its GPS location using EXIFTOOL and converts it to decimal values to call the google geocode api for location info in JSON format. I have pieced the script together from examples online.

I have been searching through the forum to find an example that I can understand on how extra certain values from the JSON data.

The first half of the script works as expected and I can use “JSON” Location Helper to get the JSON file.

I can get the first, second, third records etc. but not values inside each record. The first record is usually the formatted_address of the location. This is helpful but I would like to favour points of interest, parks, trail info etc.

I would like to search the JSON file for the specific value using these keys and default to city if none are available.

This is an example of JSON file from google.

I believe this is something simple something very simple but do not think I am searching the correct terms.

{results:{{formatted_address:"Canyonlands National Park, Utah 84532, USA", plus_code:{global_code:"85CGF55H+WH"}, address_components:{{short_name:"Canyonlands National Park", long_name:"Canyonlands National Park", types:{"establishment", "point_of_interest", "transit_station"}}, {short_name:"San Juan County", long_name:"San Juan County", types:{"administrative_area_level_2", "political"}}, {short_name:"UT", long_name:"Utah", types:{"administrative_area_level_1", "political"}}, {short_name:"US", long_name:"United States", types:{"country", "political"}}, {short_name:"84532", long_name:"84532", types:{"postal_code"}}}, geometry:{viewport:{northeast:{lat:38.461120980292, lng:-109.819667019708}, southwest:{lat:38.458423019708, lng:-109.822364980292}}, location:{lat:38.459772, lng:-109.821016}, location_type:"GEOMETRIC_CENTER"}, place_id:"ChIJcWINmO8QSIcRBYifYqyRZwU", types:{"establishment", "point_of_interest", "transit_station"}}, {formatted_address:"Grand View Point Rd, Moab, UT 84532, USA", address_components:{{short_name:"Grand View Point Rd", long_name:"Grand View Point Road", types:{"route"}}, {short_name:"Moab", long_name:"Moab", types:{"locality", "political"}}, {short_name:"San Juan County", long_name:"San Juan County", types:{"administrative_area_level_2", "political"}}, {short_name:"UT", long_name:"Utah", types:{"administrative_area_level_1", "political"}}, {short_name:"US", long_name:"United States", types:{"country", "political"}}, {short_name:"84532", long_name:"84532", types:{"postal_code"}}}, geometry:{location_type:"GEOMETRIC_CENTER", viewport:{northeast:{lat:38.4611096, lng:-109.819628219708}, southwest:{lat:38.4549744, lng:-109.822326180292}}, |bounds|:{northeast:{lat:38.4611096, lng:-109.8200543}, southwest:{lat:38.4549744, lng:-109.8219001}}, location:{lat:38.4578519, lng:-109.8200927}}, place_id:"ChIJb0bEUe4QSIcRumj4B4HcOl8", types:{"route"}}, {formatted_address:"Moab, UT 84532, USA", postcode_localities:{"Castle Valley", "Moab"}, address_components:{{short_name:"84532", long_name:"84532", types:{"postal_code"}}, {short_name:"Moab", long_name:"Moab", types:{"locality", "political"}}, {short_name:"UT", long_name:"Utah", types:{"administrative_area_level_1", "political"}}, {short_name:"US", long_name:"United States", types:{"country", "political"}}}, geometry:{location_type:"APPROXIMATE", viewport:{northeast:{lat:38.8717289, lng:-109.0588149}, southwest:{lat:37.94981, lng:-110.0574079}}, |bounds|:{northeast:{lat:38.8717289, lng:-109.0588149}, southwest:{lat:37.94981, lng:-110.0574079}}, location:{lat:38.5719944, lng:-109.4735066}}, place_id:"ChIJkVfE9H3CR4cRimQaBsHawOA", types:{"postal_code"}}, {formatted_address:"San Juan County, UT, USA", address_components:{{short_name:"San Juan County", long_name:"San Juan County", types:{"administrative_area_level_2", "political"}}, {short_name:"UT", long_name:"Utah", types:{"administrative_area_level_1", "political"}}, {short_name:"US", long_name:"United States", types:{"country", "political"}}}, geometry:{location_type:"APPROXIMATE", viewport:{northeast:{lat:38.4999896, lng:-109.0410581}, southwest:{lat:36.9979031, lng:-111.4122939}}, |bounds|:{northeast:{lat:38.4999896, lng:-109.0410581}, southwest:{lat:36.9979031, lng:-111.4122939}}, location:{lat:37.4634157, lng:-109.7591675}}, place_id:"ChIJ9Tcq0CXRN4cR55BS0_WTG_4", types:{"administrative_area_level_2", "political"}}, {formatted_address:"Utah, USA", address_components:{{short_name:"UT", long_name:"Utah", types:{"administrative_area_level_1", "political"}}, {short_name:"US", long_name:"United States", types:{"country", "political"}}}, geometry:{location_type:"APPROXIMATE", viewport:{northeast:{lat:42.001618, lng:-109.0410581}, southwest:{lat:36.9979031, lng:-114.0529979}}, |bounds|:{northeast:{lat:42.001618, lng:-109.0410581}, southwest:{lat:36.9979031, lng:-114.0529979}}, location:{lat:39.3209801, lng:-111.0937311}}, place_id:"ChIJzfkTj8drTIcRP0bXbKVK370", types:{"administrative_area_level_1", "political"}}, {formatted_address:"United States", address_components:{{short_name:"US", long_name:"United States", types:{"country", "political"}}}, geometry:{location_type:"APPROXIMATE", viewport:{northeast:{lat:71.5388001, lng:-66.885417}, southwest:{lat:18.7763, lng:170.5957}}, |bounds|:{northeast:{lat:71.5388001, lng:-66.885417}, southwest:{lat:18.7763, lng:170.5957}}, location:{lat:37.09024, lng:-95.712891}}, place_id:"ChIJCzYy5IS16lQRQrfeQ5K5Oxw", types:{"country", "political"}}, {formatted_address:"85CGF55H+GX", plus_code:{global_code:"85CGF55H+GX"}, address_components:{{short_name:"85CGF55H+GX", long_name:"85CGF55H+GX", types:{"plus_code"}}}, geometry:{location_type:"ROOFTOP", viewport:{northeast:{lat:38.460161480292, lng:-109.818713519708}, southwest:{lat:38.457463519708, lng:-109.821411480292}}, |bounds|:{northeast:{lat:38.458875, lng:-109.82}, southwest:{lat:38.45875, lng:-109.820125}}, location:{lat:38.4588056, lng:-109.8200444}}, place_id:"GhIJX5xTJLo6Q0ARJtGBm3t0W8A", types:{"plus_code"}}}, status:"OK", plus_code:{global_code:"85CGF55H+GX"}}

You can access nested values with the help of Objective-C’s key paths.

Assuming thePath contains the path to a file containing the JSON data you can get the values of all “lat” keys with


use AppleScript version "2.5"
use framework "Foundation"
use scripting additions

property || : a reference to current application

set jsonData to ||'s NSData's dataWithContentsOfFile:thePath
set {jsonDictionary, jsonError} to ||'s NSJSONSerialization's JSONObjectWithData:jsonData options:0 |error|:(reference)
if jsonError is not missing value then error (jsonError's localizedDescription()) as text
set results to jsonDictionary's objectForKey:"results"
set allLats to (results's valueForKeyPath:"geometry.viewport.northeast.lat") as list


Thank you I will have to read up on this so I can understand it a little better.

Hopefully I will get some time this evening to work on it.

For my clarification, JSON helper or Location helper only make it easier to get the whole JSON file into AppleScript? They do not help with parse individual values from inside the JSON file? I thought my syntax was wrong since I could extract “formatted_address” but whenever I tried to change it to something else it failed.

set api_key to "XXXXXXXXXXXXXXXXXXX"

tell application "Location Helper"
	
	set jsonData to reverse geocode location using coordinates {latDecimal, lonDecimal} with API key api_key as string
	
	set jsonCountry to formatted_address of item 1 of results of jsonData
	
end tell

return {hazelOutputAttributes:{jsonCountry}}

A light just turned on for how to call for specific items using the valueForKeyPath.

I will have a closer look later.

You have to convert the AppleScript dictionary to an AppleScriptObjC (Foundation) dictionary to be able to use valueForKeyPath

set foundationDictionary to current application's NSDictionary's dictionaryWithDictionary:applescriptDictionary

Here is my script. I get the error that

set ExifTool to "/usr/local/bin/exiftool"
set ExifGPSLat to "-GPSLatitude"
set ExifGPSLon to "-GPSLongitude"

set pathList to ""

try
	-- Manual
	set pathList to quoted form of POSIX path of (choose file with prompt "Please choose a file:" default location (path to desktop))
	-- Hazel
	--set pathList to quoted form of POSIX path of (theFile as alias)
end try

-- Get Camera Model
--set cameraModel to do shell script ExifTool & " -p '${model}' " & pathList

--Get GPS Latitude and Longitude from EXIF data
do shell script ExifTool & space & ExifGPSLat & space & "-overwrite_original_in_place -P" & space & pathList
set gpsLat to result

do shell script ExifTool & space & ExifGPSLon & space & "-overwrite_original_in_place -P" & space & pathList
set gpsLon to result

--

if (gpsLat = "") or (gpsLon = "") then
	-- error number -128
	set trail to ""
	set POI to ""
	set country to ""
	set state to "no gps"
	set cityTown to ""
	set jsonCountry to ""
	return {hazelPassesScript:false, hazelOutputAttributes:{trail, POI, state, country, cameraModel}}
	
else
	-- Set GPS variables separately
	set latDegree to word 3 of gpsLat
	set latMinute to word 5 of gpsLat
	set latSecond to word 6 of gpsLat
	set latDirection to word 7 of gpsLat
	set lonDegree to word 3 of gpsLon
	set lonMinute to word 5 of gpsLon
	set lonSecond to word 6 of gpsLon
	set lonDirection to word 7 of gpsLon
	
	-- Convert Deg,Sec,Min to Decimals
	set latDecimal to latDegree + latMinute / 60 + latSecond / 3600
	set lonDecimal to lonDegree + lonMinute / 60 + lonSecond / 3600
	-- Change Decimals based on Direction
	if (latDirection is equal to "S") then
		set latDecimal to latDecimal * -1 as number --text
	end if
	
	if (lonDirection is equal to "W") then
		set lonDecimal to lonDecimal * -1 as number --text
	end if
		
	-- SET GOOGLE API KEY
	set api_key to "XXXXXXXXXXXXXXXXX"
	
	tell application "Location Helper"
		
		set thePath to reverse geocode location using coordinates {latDecimal, lonDecimal} with API key api_key as string
		
	end tell
	
end if

use AppleScript version "2.5"
use framework "Foundation"
use scripting additions

property || : a reference to current application

set jsonData to ||'s NSData's dataWithContentsOfFile:thepath
set {jsonDictionary, jsonError} to ||'s NSJSONSerialization's JSONObjectWithData:jsonData options:0 |error|:(reference)
if jsonError is not missing value then error (jsonError's localizedDescription()) as text
set results to jsonDictionary's objectForKey:"results"
set allLats to (results's valueForKeyPath:"geometry.viewport.northeast.lat") as list


return {hazelOutputAttributes:{allLats}}

With the use of AppleScriptObjC do I still need to parse the file with JSON helper or will the following code be enough?

I read somewhere about passing dataWithContentsOfFile vs the dataWithContentsOfURL. Since I am passing the google api url directly should I be using OfFile or OfURL?

Using OfFile I get the error

Using OfURL I get the error

Alternatively returning jsonData returns missing value.

set api_key to "XXXXXXXXXXXXXXX"

set locatep to "38.4588056" & "," & "-109.8200444" as string

set thepath to "https://maps.googleapis.com/maps/api/geocode/json?latlng=" & locatep & "&key=" & api_key

use AppleScript version "2.5"
use framework "Foundation"
use scripting additions

property || : a reference to current application

set jsonData to ||'s NSData's dataWithContentsOfFile:thepath
set {jsonDictionary, jsonError} to ||'s NSJSONSerialization's JSONObjectWithData:jsonData options:0 |error|:(reference)
if jsonError is not missing value then error (jsonError's localizedDescription()) as text
set results to jsonDictionary's objectForKey:"results"
set allLats to (results's valueForKeyPath:"geometry.viewport.northeast.lat") as list
return allLats

The API dataWithContentsOfFile is for file systems paths.

You have to create an URL and use dataWithContentsOfURL

set thepath to "https://maps.googleapis.com/maps/api/geocode/json?latlng=" & locatep & "&key=" & api_key
set theURL to ||'s NSURL's URLWithString:thepath
set jsonData to ||'s NSData's dataWithContentsOfURL:theURL

Thank you, I have this working now but was hoping I could ask one more question.

In your example you are drilling down level by level to extract all of the latitudes inside northeast inside viewport inside geometry for each item which I understand.

set allLats to (results's valueForKeyPath:"geometry.viewport.northeast.lat") as list

However, how would you extract the long_name of specific “types” if the record resides at the same level? For example if I wanted to extract the long_name of administrative_area_level_1 or I wanted to search for Park or Establishment.

I really appreciate your help. I have been trying to figure something efficient out off and on for a long time.

Unfortunately due to the nested arrays it’s not possible to search with key paths.

This is an example with a loop, asDict represents the AppleScript dictionary in your first post.

longNames will contain all long names whose types contain administrative_area_level_1


use AppleScript version "2.5"
use framework "Foundation"
use scripting additions

property || : a reference to current application

set cocoaDict to ||'s NSDictionary's dictionaryWithDictionary:asDict
set results to cocoaDict's objectForKey:"results"
set longNames to {}
repeat with anItem in results
	set addressComponents to (anItem's objectForKey:"address_components")
	repeat with aComponent in addressComponents
		set componentTypes to (aComponent's objectForKey:"types")
		if (componentTypes's containsObject:"administrative_area_level_1") then
			set end of longNames to (aComponent's objectForKey:"long_name") as text
		end if
	end repeat
end repeat

Stefan, thank you this is great.

I have been testing it and playing with the types returned and works like a charm inside AppleScript Editor. Thank you very much.

Are you familiar with Hazel? I cannot get the script to compile inside the Hazel handler

on hazelMatchFile(theFile, inputAttributes)
--Code
return {hazelPassesScript:true, hazelOutputAttributes:{}}


The script gets stuck on the following lines. If I comment out the handler all is well.

use AppleScript version "2.5"
use framework "Foundation"
use scripting additions

property || : a reference to current application

Here is my final script.

on hazelMatchFile(theFile, inputAttributes)
	
	set ExifTool to "/usr/local/bin/exiftool"
	set ExifGPSLat to "-GPSLatitude"
	set ExifGPSLon to "-GPSLongitude"
	set ExifToolOption to "-keywords="
	
	set pathList to ""
	
	set TagTool to "/usr/local/bin/tag"
	set TagToolOption to "-s" -- The set operation reTrails all tags on the specified files with one or more new tags.
	set TagToolNOGPS to "NO GPS Information"
	
	try
		-- Manual
		--set pathList to quoted form of POSIX path of (choose file with prompt "Please choose a file:" default location (path to desktop))
		-- Hazel
		set pathList to quoted form of POSIX path of (theFile as alias)
	end try
	
	-- Get Camera Model
	--set cameraModel to do shell script ExifTool & " -p '${model}' " & pathList
	
	--Get GPS Latitude and Longitude from EXIF data
	do shell script ExifTool & space & ExifGPSLat & space & "-overwrite_original_in_place -P" & space & pathList
	set gpsLat to result
	
	do shell script ExifTool & space & ExifGPSLon & space & "-overwrite_original_in_place -P" & space & pathList
	set gpsLon to result
	
	--do shell script TagTool & space & TagToolOption & quoted form of TagToolNOGPS & space & pathList
	
	-- CONVERT GPS COORDINDATES TO DECIMAL
	if (gpsLat = "") or (gpsLon = "") then
		-- error number -128
		set trail to ""
		set poi to ""
		set country to ""
		set state to "no gps"
		set cityTown to ""
		set jsonCountry to ""
		return {hazelPassesScript:false, hazelOutputAttributes:{trail, poi, state, country, cameraModel}}
		
	else
		-- Set GPS variables separately
		set latDegree to word 3 of gpsLat
		set latMinute to word 5 of gpsLat
		set latSecond to word 6 of gpsLat
		set latDirection to word 7 of gpsLat
		set lonDegree to word 3 of gpsLon
		set lonMinute to word 5 of gpsLon
		set lonSecond to word 6 of gpsLon
		set lonDirection to word 7 of gpsLon
		
		-- Convert Deg,Sec,Min to Decimals
		set latDecimal to latDegree + latMinute / 60 + latSecond / 3600
		set lonDecimal to lonDegree + lonMinute / 60 + lonSecond / 3600
		-- Change Decimals based on Direction
		if (latDirection is equal to "S") then
			set latDecimal to latDecimal * -1 as number --text
		end if
		
		if (lonDirection is equal to "W") then
			set lonDecimal to lonDecimal * -1 as number --text
		end if
		
		set locateP to latDecimal & "," & lonDecimal as string
		
		-- SET GOOGLE API KEY
		set api_key to "AIzaSyCewHXy9mUtqRzUAIY036xxDoAh-26JJys"
		
		set thePath to "https://maps.googleapis.com/maps/api/geocode/json?latlng=" & locateP & "&key=" & api_key
		
		tell application "Location Helper"
			set asDict to reverse geocode location using coordinates {latDecimal, lonDecimal} with API key api_key
		end tell
		
		--tell application "Safari" to open location thePath
	end if

use AppleScript version "2.5"
use framework "Foundation"
use scripting additions

property || : a reference to current application

set cocoaDict to ||'s NSDictionary's dictionaryWithDictionary:asDict
set results to cocoaDict's objectForKey:"results"

set poi to {}
set poiTemp to {}

set route to {}
set routeTemp to {}

set cityTown to {}
set cityTownTemp to {}

set stateProvince to {}
set stateProvinceTemp to {}

set country to {}
set countryTemp to {}

repeat with anItem in results
	set addressComponents to (anItem's objectForKey:"address_components")
	repeat with aComponent in addressComponents
		set componentTypes to (aComponent's objectForKey:"types")
		if (componentTypes's containsObject:"point_of_interest") then
			set end of poiTemp to (aComponent's objectForKey:"long_name") as text
			set poi to item 1 of poiTemp
		else
			set componentTypes to (aComponent's objectForKey:"types")
			if (componentTypes's containsObject:"establishment") then
				set end of poiTemp to (aComponent's objectForKey:"long_name") as text
				set poi to item 1 of poiTemp
			else
				set componentTypes to (aComponent's objectForKey:"types")
				if (componentTypes's containsObject:"park") then
					set end of poiTemp to (aComponent's objectForKey:"long_name") as text
					set poi to item 1 of poiTemp
				else
					set componentTypes to (aComponent's objectForKey:"types")
					if (componentTypes's containsObject:"natural_feature") then
						set end of poiTemp to (aComponent's objectForKey:"long_name") as text
						set poi to item 1 of poiTemp
					else
						set componentTypes to (aComponent's objectForKey:"types")
						if (componentTypes's containsObject:"colloquial_area") then
							set end of poiTemp to (aComponent's objectForKey:"long_name") as text
							set poi to item 1 of poiTemp
						end if
					end if
				end if
			end if
		end if
		if (componentTypes's containsObject:"route") then
			set end of routeTemp to (aComponent's objectForKey:"long_name") as text
			set route to item 1 of routeTemp
		else
			if (componentTypes's containsObject:"premise") then
				set end of routeTemp to (aComponent's objectForKey:"long_name") as text
				set route to item 1 of routeTemp
			end if
		end if
		if (componentTypes's containsObject:"locality") then
			set end of cityTownTemp to (aComponent's objectForKey:"long_name") as text
			set cityTown to item 1 of cityTownTemp
		end if
		if (componentTypes's containsObject:"administrative_area_level_1") then
			set end of stateProvinceTemp to (aComponent's objectForKey:"long_name") as text
			set stateProvince to item 1 of stateProvinceTemp
		end if
		if (componentTypes's containsObject:"country") then
			set end of countryTemp to (aComponent's objectForKey:"long_name") as text
			set country to item 1 of countryTemp
		end if
	end repeat
end repeat

return {hazelPassesScript:true, hazelOutputAttributes:{route, poi, cityTown, stateProvince, country}}

I’m afraid I’m not familiar with Hazel