Skip to content

Instantly share code, notes, and snippets.

@RadGH
Last active July 19, 2023 19:16
Show Gist options
  • Star 20 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
  • Save RadGH/428bd8133c34dae60c0c to your computer and use it in GitHub Desktop.
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
<?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>: &ldquo;' . $full_address . '&rdquo; (<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;
}
@ktoombs-sm2dev
Copy link

This is a very nifty tool! I'm using multiple fields for the address and I am able to see the latitude and longitude are being generated on the /?acf-recalc-locations page. I would like to take that data (lat/lng) and populate two respective ACF fields of the custom post type via update_post_meta(). I tried multiple different ways but all of my attempts have failed. Do you know how I could accomplish this? How exactly do I grab that 'latitude' and 'longitude' meta keys that are generated?

@jvwrx
Copy link

jvwrx commented Apr 8, 2021

@sm2dev Pretty sure that's already built in, you just need to modify the second values on lines 39 and 40 to be your ACF fields, such as... 'latitude_key' => 'myLatitudeField' instead of 'latitude_key' => 'latitude'

It's a little confusing because they are currently called "longitude" and "latitude", but all of your posts likely already have them saved in as (non-ACF) custom fields.

If you do change the lat/long custom fields, be sure to change the 'scan_identifier' so the coords get saved in your ACF custom fields when you rescan.

@ktoombs-sm2dev
Copy link

@jvwrx thanks. Is there a way to generate lat/lng with this code without the need to visit /?acf-recalc-locations to generate those? We have over tens of thousands of locations to import, with new imports on a bi-weekly basis.

@jvwrx
Copy link

jvwrx commented Apr 8, 2021

@sm2dev If only! It's on my to-do list to find a better way, but currently I have a job set at uptimerobot.com that visits the page regularly and reports back to me if the keyword "error" appears (which means I need to load the page manually and see what's up)

@ktoombs-sm2dev
Copy link

@jvwrx bummer. I guess I'll keep my eyes open for that ability, or alternative options.

@RadGH
Copy link
Author

RadGH commented Apr 12, 2021

@sm2dev It sounds like you have 10,000+ locations and already ran the tool, and don't want to run it again just to change keys. Is that right?

If so, this might work. It is not tested.

// Get posts that still use the old key "latitude"
$args = array(
  'post_type' => 'POSTTYPE',
  'posts_per_page' => 200,
  'meta_query' => array(
    array(
      'key' => 'latitude',
      'compare' => 'exists',
    ),
  ),
);

$q = new WP_Query($args);

if ( false == $q->have_posts() ) {
  echo 'done!';
  exit;
}

// Loop through each post and meta key from "latitude" to "my_custom_lat"
foreach( $q->posts as $p ) {
  echo 'Updated: ' . $p->post_title . ' (#'. $p->ID .')<br>';

  $v = get_post_meta( $p->ID, "latitude", true );
  update_post_meta( $p->ID, "my_custom_lat", $v);
  delete_post_meta( $p->ID, "latitude" );

  $v = get_post_meta( $p->ID, "longitude", true );
  update_post_meta( $p->ID, "my_custom_lng", $v);
  delete_post_meta( $p->ID, "longitude" );
}

// Refresh the page to do the next batch
echo 'Batch complete, reloading page for next batch';
echo '<script type="text/javascript">window.location.reload();</script>';

If you need to update lat/lng when an address changes you'll need to rework my original code and set it up with a filter like "acf/update_value" for your address fields. Say the address changes, you can geocode the new address and update the keys.

@ktoombs-sm2dev
Copy link

@RadGH Thanks for the tips and test code. We have not run the tool on the 11,000+ locations. We have only used the tool on a few test locations to confirm it worked. Upon importing from a CSV we would need the tool to fire so that all 11,000+ locations generate their lat/lng on post creation without needing to visit /?acf-recalc-locations to generate them. We were planning to use the plugin WP All Import.

I also noticed that sometimes the combined full address is not being input into the Google Map ACF field type. The tool does generate the lat/lng from the single text fields (address, address2, city, state, zip) but combining them into the Google Map field so that the location shows on the map of each post is not working. Do you know of any reasons why it would work on some posts and not others?

@RadGH
Copy link
Author

RadGH commented Apr 13, 2021

@sm2dev

"Upon importing from a CSV we would need the tool to fire so that all 11,000+ locations generate their lat/lng on post creation without needing to visit /?acf-recalc-locations to generate them. We were planning to use the plugin WP All Import."

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.

"I also noticed that sometimes the combined full address is not being input into the Google Map ACF field type."

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.

@ktoombs-sm2dev
Copy link

@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