Last active
July 19, 2023 19:16
-
-
Save RadGH/428bd8133c34dae60c0c to your computer and use it in GitHub Desktop.
Get latitude and longitude for addresses saved in Advanced Custom Fields, using Google's GeoLocation API
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
global $acf_recalc_settings; | |
// IMPORTANT: Customize these settings for your website. | |
$acf_recalc_settings = array( | |
// How many updates to do each page load. As of November 2018, Google's GeoLocation API limit is 100 per second. | |
'posts_per_run' => 16, | |
// The post type which has the addresses (This code can be adapted for users or terms, but these settings are designed for Custom Post Types). | |
'post_types' => array( 'post' ), | |
// GeoLocation API Key is required as of November 2018. Get an api key here: https://developers.google.com/maps/documentation/geolocation/get-api-key | |
// More info from google here: http://g.co/dev/maps-no-account | |
'geolocation_api_key' => 'ENTER_YOUR_API_KEY', | |
// Custom field mapping. Put your field names or field keys as the values. If you do not use one, just leave the value blank. | |
'fields' => array( | |
// If you use a Google Map field to store the latitude/longitude: | |
'google_map' => 'field_5de9a6b17b438', | |
// Address field that will be used for Geo Location | |
'address' => 'field_5de9a2797a43c', | |
// Optional. If your address is split into multiple fields, list them here. You can leave these blank. | |
'address2' => 'field_5de9a9e3eccb5', | |
// Second line of an address usually for Apartment/Suite number | |
'city' => 'field_5de9a679cc0e4', | |
'state' => 'field_5de9a678cc0e3', | |
'zip' => 'field_5de9a678cc0e2', | |
'country' => 'field_5de9a9ddeccb4', | |
), | |
// Latitude and Longitude will be saved to the google map field, but you probably want them as separate meta keys as well if you ever need to sort by distance or that sort of thing. | |
// This should be compatible with update_post_meta(), do not use ACF Field Keys. | |
'latitude_key' => 'latitude', | |
'longitude_key' => 'longitude', | |
// Change this if you want to repeat the scan for items that were already updated. | |
'scan_identifier' => '20191205', | |
); | |
function recalc_acf_locations_init() { | |
if ( !isset( $_REQUEST['acf-recalc-locations'] ) ) return; | |
global $acf_recalc_settings; | |
$args = array( | |
'post_type' => $acf_recalc_settings['post_types'], | |
'meta_query' => array( | |
'relation' => 'OR', | |
array( | |
'key' => 'acf-recalculated-scan', | |
'compare' => 'NOT EXISTS', | |
'value' => ' ', | |
), | |
array( | |
'key' => 'acf-recalculated-scan', | |
'compare' => '!=', | |
'value' => $acf_recalc_settings['scan_identifier'], | |
), | |
), | |
'posts_per_page' => $acf_recalc_settings['posts_per_run'], | |
); | |
$locations = new WP_Query( $args ); | |
if ( ! $locations->have_posts() ) { | |
wp_die( '<p><strong>ACF Recalc Locations:</strong> All locations have latitude/longitude present! All done!</p>' ); | |
exit; | |
} | |
echo '<h2>Scanning ' . min( $locations->found_posts, $acf_recalc_settings['posts_per_run'] ) . ' of ' . $locations->found_posts . '...</h2>'; | |
echo '<div style="font-size: 14px; font-family: Arial, sans-serif;">'; | |
foreach( $locations->posts as $i => $p ) { | |
echo '<div style="width: 50%; float: left; overflow: auto;">'; | |
$url = get_permalink( $p->ID ); | |
$edit = get_edit_post_link( $p->ID ); | |
echo '<p><strong>' . ( $i + 1 ) . ') ' . ucwords( str_replace( array( | |
'-', | |
'_', | |
), ' ', $p->post_type ) ) . ' #' . $p->ID . '</strong> - <a href="' . esc_attr( $url ) . '" target="_blank">' . esc_html( $p->post_title ) . '</a> (<a href="' . esc_attr( $edit ) . '" target="_blank">Edit</a>)</p>'; | |
echo '<pre style="border-bottom: 1px solid #666; padding-bottom: 15px; margin: 15px 0;">'; | |
recalc_acf_location_single( $p->ID ); | |
echo '</pre>'; | |
echo '</div>'; | |
} | |
echo '</div>'; | |
exit; | |
} | |
add_action( 'init', 'recalc_acf_locations_init' ); | |
/** | |
* Collects the full address of a location, then retrieves the Latitude/Longitude | |
* | |
* @param $post_id | |
* | |
* @return bool | |
*/ | |
function recalc_acf_location_single( $post_id ) { | |
global $acf_recalc_settings; | |
$location = get_field( $acf_recalc_settings['fields']['google_map'], $post_id ); | |
$lookup_address = null; | |
$result = null; | |
$full_address = null; | |
// Get the address from custom fields that are set | |
$f = $acf_recalc_settings['fields']; | |
$address = $f['address'] ? get_field( $f['address'], $post_id ) : ""; | |
$address2 = $f['address2'] ? get_field( $f['address2'], $post_id ) : ""; | |
$city = $f['city'] ? get_field( $f['city'], $post_id ) : ""; | |
$state = $f['state'] ? get_field( $f['state'], $post_id ) : ""; | |
$zip = $f['zip'] ? get_field( $f['zip'], $post_id ) : ""; | |
$country = $f['country'] ? get_field( $f['country'], $post_id ) : ""; | |
// Combine all pieces above, and remove any that are empty using array_filter | |
$parts = array_filter( array( | |
$address, | |
$address2, | |
$city, | |
$state, | |
$zip, | |
$country, | |
) ); | |
// Join the array with comma separation | |
$full_address = implode( ', ', $parts ); | |
if ( empty( $full_address ) ) { | |
// Give an error if an address is invalid which halts the process so it can be fixed manually. | |
echo '<strong>ERROR:</strong> Location does not have a valid google map address, and no fallback address fields are given. Aborting operation, consider fixing this manually.'; | |
exit; | |
}else{ | |
// Show the full address that we determined | |
$map_url = add_query_arg( array( 'q' => rawurlencode( $full_address ) ), 'https://maps.google.com/' ); | |
echo '<strong>Address</strong>: “' . $full_address . '” (<a href="' . esc_attr( $map_url ) . '" target="_blank">map it</a>)<br>'; | |
} | |
// Get the latitude/longitude for that address, and save it to the post | |
$result = recalc_acf_location_lookup( $post_id, $full_address ); | |
if ( $result ) { | |
if ( $result === true ) { | |
echo 'Location was already up to date.'; | |
}else{ | |
echo 'Location has been found and saved: ' . esc_html( $result['lat'] ) . ', ' . esc_html( $result['lng'] ) . '.'; | |
return true; | |
} | |
}else{ | |
echo '<h2>ERROR! Google map could not locate this address. Aborting operation.</h2>'; | |
exit; | |
} | |
} | |
function recalc_acf_location_lookup( $post_id, $full_address ) { | |
global $acf_recalc_settings; | |
$address_one_line = preg_replace( '/ *(\r\n|\r|\n)+ */', " ", $full_address ); | |
$coords = recalc_acf_get_latlng( $address_one_line ); | |
if ( $coords ) { | |
$location = get_field( $acf_recalc_settings['fields']['google_map'], $post_id ); | |
if ( !is_array( $location ) ) { | |
$location = array( | |
'address' => '', | |
'lat' => '', | |
'lng' => '', | |
); | |
} | |
if ( empty( $location['address'] ) ) { | |
$location['address'] = $address_one_line; | |
}else{ | |
if ( $address_one_line === $location['address'] && $location['lat'] === $coords['lat'] && $location['lng'] === $coords['lng'] ) { | |
// Save the scan identiifer which will prevent repeating on the same post unless the identifier changes | |
update_post_meta( $post_id, 'acf-recalculated-scan', $acf_recalc_settings['scan_identifier'] ); | |
return true; | |
} | |
} | |
$location['lat'] = $coords['lat']; | |
$location['lng'] = $coords['lng']; | |
$result = update_field( $acf_recalc_settings['fields']['google_map'], $location, $post_id ); | |
if ( $result ) { | |
// Save lat/long as separate meta keys | |
update_post_meta( $post_id, $acf_recalc_settings['latitude_key'], $location['lat'] ); | |
update_post_meta( $post_id, $acf_recalc_settings['longitude_key'], $location['lng'] ); | |
// Save the scan identiifer which will prevent repeating on the same post unless the identifier changes | |
update_post_meta( $post_id, 'acf-recalculated-scan', $acf_recalc_settings['scan_identifier'] ); | |
return $location; | |
}else{ | |
// Give an error that requires manual attention. | |
echo '<h2>ERROR! Failed to locate map location for post ID #' . $post_id . '.</h2><p>Please review this entry manually to continue.</p>'; | |
exit; | |
} | |
} | |
return false; | |
} | |
function recalc_acf_get_latlng( $address ) { | |
global $acf_recalc_settings; | |
// http://stackoverflow.com/a/8633623/470480 | |
$address = urlencode( $address ); // Spaces as + signs | |
// Legacy API -- No longer supported | |
// $geolocation_url = "http://maps.google.com/maps/api/geocode/json?address=$address&sensor=false"; | |
// November 2018 update with api key support and higher limits | |
$geolocation_url = "https://maps.googleapis.com/maps/api/geocode/json?address=$address&key=" . $acf_recalc_settings['geolocation_api_key']; | |
$request = wp_remote_get( $geolocation_url ); | |
$json = wp_remote_retrieve_body( $request ); | |
if ( !$json ) { | |
echo 'Google Maps returned an empty response'; | |
return false; | |
} | |
$data = json_decode( $json ); | |
if ( !$data ) { | |
echo '<h2>ERROR! Google Maps returned an invalid response, expected JSON data:</h2>'; | |
echo esc_html( print_r( $json, true ) ); | |
exit; | |
} | |
if ( isset( $data->{'error_message'} ) ) { | |
echo '<h2>ERROR! Google Maps API returned an error:</h2>'; | |
echo '<strong>' . esc_html( $data->{'status'} ) . '</strong> ' . esc_html( $data->{'error_message'} ) . '<br>'; | |
exit; | |
} | |
if ( empty( $data->{'results'}[0]->{'geometry'}->{'location'}->{'lat'} ) || empty( $data->{'results'}[0]->{'geometry'}->{'location'}->{'lng'} ) ) { | |
echo '<h2>ERROR! Latitude/Longitude could not be found:</h2>'; | |
echo esc_html( print_r( $data, true ) ); | |
exit; | |
} | |
$lat = $data->{'results'}[0]->{'geometry'}->{'location'}->{'lat'}; | |
$lng = $data->{'results'}[0]->{'geometry'}->{'location'}->{'lng'}; | |
// Value can be negative, so check for specifically 0. | |
if ( floatval( $lat ) === 0 || floatval( $lng ) === 0 ) { | |
echo '<h2>ERROR! Latitude/Longitude is invalid (exactly zero):</h2>'; | |
var_dump( 'Latitude:', $lat ); | |
var_dump( 'Longitude:', $lng ); | |
var_dump( 'Result:', $data->{'results'}[0] ); | |
exit; | |
} | |
return array( | |
'lat' => $lat, | |
'lng' => $lng, | |
); | |
} | |
function recalc_acf_clean_address( $address ) { | |
$address = preg_replace( '/ *(\r\n|\r|\n)+ */', " ", $address ); // No linebreaks | |
return $address; | |
} |
@RadGH Thanks for the info. This project was put on hold temporarily but I'll look at slashing code since I'm using WP All Import. Thanks!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@sm2dev
WP All Import will work great for that. You can scrap 90% of my code if you go that route since you just need to get address, geolocate, save result. Just be careful as it might slow WP All Import down, or you might hit rate limits where the geolocation fails if it goes too quickly.
Or import them first, then run my tool. Either way it's a one-off batch process.
Sorry, no idea off the top of my head. I don't use map fields very often and don't have the time to look into that further right now. I would suggest poking around the postmeta table for one of the posts with no map pin. Try to spot the difference from a working post, then figure out what is missing.
The code I provided originally was not designed for a map field. The longitude/latitude are stored in separate meta keys. The point of that is so you can use "geo_query" with the lng/lat to calculate distance from a search address.
You might need to put those meta keys in the map field some other way - probably a serialized array.