Let’s discuss and show how using ACF Google Maps, and WP_Query
we can search content by radius and/or proximity using both latitude and longitude set through ACF Google Maps.
File set up:
- Lets create three seperate files ( these will be included inside your
functions.php
in order ) sort-acf-gmap-distance.php
wp-query-geo.php
sort-locations-api.php
Let’s do a quick review of what the following will consist of:
Sort ACF Gmap Distance
This file will contain a function where we send the address to be geocoded, and will then return that locations lat
and lng
. It also saves the them as separate meta fields.
WP Query GEO
This file will contains multiple functions and notes. Here is a post that describes how it all works a little more. The point is that it provides another query
option, geo_query
. With this we can direct queries to the DB and search specifically for lat/lng will still doing normal a meta_query
, etc. Essentially you aren’t limited to just that new query when filtering content.
Sort Locations API
This file will contain our newly created API endpoint that uses all of the above and outputs the desired results. This will also include pagination.
That all sound good? Then lets roll!
sort-acf-gmap-distancy.php
<?php // returns longitude and latitude from a location function fancysquares_get_lat_and_lng($origin){ $api_key = "<your-api-key>"; $url = "https://maps.googleapis.com/maps/api/geocode/json?address=".urlencode($origin)."&key=".$api_key; $result_string = file_get_contents($url); $result = json_decode($result_string, true); $result1[]=$result['results'][0]; return $result1[0]; } // update unique meta field with ACF Google Map LAT / LNG function acf_save_update_latlng($post_id, $post, $update) { $map = get_post_meta($post_id, 'your_map', true); if (!empty($map)) { update_post_meta( $post_id, 'flat_lat', $map['lat'] ); update_post_meta( $post_id, 'flat_lng', $map['lng'] ); } } add_action('save_post', 'acf_save_update_latlng', 90, 3); // run after ACF saves the $_POST['acf'] data add_action('acf/save_post', 'acf_save_lat_lng', 20);
wp-query-geo.php
Please see here for additional references from their comments:
<?php if(!defined('ABSPATH')) { die(); } // Include in all php files, to prevent direct execution /** * Plugin Name: WP Geo Query * Plugin URI: https://gschoppe.com/wordpress/location-searches/ * Description: Adds location search support to WP_Query, making it easy to create completely custom "Find Location" pages. * Author: Greg Schoppe * Author URI: https://gschoppe.com * Version: 1.0.0 **/ if( !class_exists('GJSGeoQuery') ) { class GJSGeoQuery { public static function Instance() { static $instance = null; if ($instance === null) { $instance = new self(); } return $instance; } private function __construct() { add_filter( 'posts_fields' , array( $this, 'posts_fields' ), 10, 2 ); add_filter( 'posts_join' , array( $this, 'posts_join' ), 10, 2 ); add_filter( 'posts_where' , array( $this, 'posts_where' ), 10, 2 ); add_filter( 'posts_orderby', array( $this, 'posts_orderby' ), 10, 2 ); } // add a calculated "distance" parameter to the sql query, using a haversine formula public function posts_fields( $sql, $query ) { global $wpdb; $geo_query = $query->get('geo_query'); if( $geo_query ) { if( $sql ) { $sql .= ', '; } $sql .= $this->haversine_term( $geo_query ) . " AS geo_query_distance"; } return $sql; } public function posts_join( $sql, $query ) { global $wpdb; $geo_query = $query->get('geo_query'); if( $geo_query ) { if( $sql ) { $sql .= ' '; } $sql .= "INNER JOIN " . $wpdb->prefix . "postmeta AS geo_query_lat ON ( " . $wpdb->prefix . "posts.ID = geo_query_lat.post_id ) "; $sql .= "INNER JOIN " . $wpdb->prefix . "postmeta AS geo_query_lng ON ( " . $wpdb->prefix . "posts.ID = geo_query_lng.post_id ) "; } return $sql; } // match on the right metafields, and filter by distance public function posts_where( $sql, $query ) { global $wpdb; $geo_query = $query->get('geo_query'); if( $geo_query ) { $lat_field = 'latitude'; if( !empty( $geo_query['lat_field'] ) ) { $lat_field = $geo_query['lat_field']; // print_r($lat_field); // die; } $lng_field = 'longitude'; if( !empty( $geo_query['lng_field'] ) ) { $lng_field = $geo_query['lng_field']; } $distance = 20; if( isset( $geo_query['distance'] ) ) { $distance = $geo_query['distance']; // print_r($distance); // die; } if( $sql ) { $sql .= " AND "; } $haversine = $this->haversine_term( $geo_query ); $new_sql = "( geo_query_lat.meta_key = %s AND geo_query_lng.meta_key = %s AND " . $haversine . " <= %f )"; $sql .= $wpdb->prepare( $new_sql, $lat_field, $lng_field, $distance ); } return $sql; } // handle ordering public function posts_orderby( $sql, $query ) { $geo_query = $query->get('geo_query'); if( $geo_query ) { $orderby = $query->get('orderby'); $order = $query->get('order'); if( $orderby == 'distance' ) { if( !$order ) { $order = 'ASC'; } $sql = 'geo_query_distance ' . $order; } } return $sql; } public static function the_distance( $post_obj = null, $round = false ) { echo self::get_the_distance( $post_obj, $round ); } public static function get_the_distance( $post_obj = null, $round = false ) { global $post; if( !$post_obj ) { $post_obj = $post; } if( property_exists( $post_obj, 'geo_query_distance' ) ) { $distance = $post_obj->geo_query_distance; if( $round !== false ) { $distance = round( $distance, $round ); } return $distance; } return false; } private function haversine_term( $geo_query ) { global $wpdb; $units = "miles"; if( !empty( $geo_query['units'] ) ) { $units = strtolower( $geo_query['units'] ); } $radius = 3959; if( in_array( $units, array( 'km', 'kilometers' ) ) ) { $radius = 6371; } $lat_field = "geo_query_lat.meta_value"; $lng_field = "geo_query_lng.meta_value"; $lat = 0; $lng = 0; if( isset( $geo_query['latitude'] ) ) { $lat = $geo_query['latitude' ]; } if( isset( $geo_query['longitude'] ) ) { $lng = $geo_query['longitude']; } $haversine = "( " . $radius . " * "; $haversine .= "acos( cos( radians(%f) ) * cos( radians( " . $lat_field . " ) ) * "; $haversine .= "cos( radians( " . $lng_field . " ) - radians(%f) ) + "; $haversine .= "sin( radians(%f) ) * sin( radians( " . $lat_field . " ) ) ) "; $haversine .= ")"; $haversine = $wpdb->prepare( $haversine, array( $lat, $lng, $lat ) ); return $haversine; } } GJSGeoQuery::Instance(); } if( !function_exists( 'the_distance' ) ) { function the_distance( $post_obj = null, $round = false ) { GJSGeoQuery::the_distance( $post_obj, $round ); } } if( !function_exists( 'get_the_distance' ) ) { function get_the_distance( $post_obj = null, $round = false ) { return GJSGeoQuery::get_the_distance( $post_obj, $round ); } }
sort-locations-api.php
<?php /** * Register the /wp-json/fancysquares/v1/vandelay_industries endpoint */ /** * get vandelay_industries by location radius, closest within 10 files to what you entered */ class FANCY_SQUARES_MAP_RESULTS_API_ENDPOINT extends WP_REST_Controller { /** * Constructor. */ public function __construct() { $this->namespace = 'fancysquares/v1'; $this->rest_base = 'vandelay_industries'; } /** * Register the component routes. */ public function register_routes() { //region added register_rest_route( $this->namespace, '/' . $this->rest_base , array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'vandelay_industries_locations' ), 'permission_callback' => array( $this, 'vandelay_industries_locations_permissions_check' ), 'args' => $this->get_collection_params(), ) ) ); } /** * Retrieve vandelay industries locations */ public function vandelay_industries_locations( $request ) { $origin = $request['location']; $proximity = $request['proximity']; $foundCity = ''; //$foundState = ''; $lat1 = ''; $lng1 = ''; if($origin && ($usingLocation === 'yes')) { $origin_coords = fancysquares_get_lat_and_lng($origin); // find city and state for ($j=0;$j<count($origin_coords['address_components']);$j++) { $cn=array($origin_coords['address_components'][$j]['types'][0]); if (in_array("locality", $cn)) { $foundCity = $origin_coords['address_components'][$j]['long_name']; } } $lat1 = $origin_coords['geometry']['location']['lat']; $lng1 = $origin_coords['geometry']['location']['lng']; $args = array( 'update_post_term_cache' => false, 'post_type' => 'vandelay_industries', 'posts_per_page' => $request['per_page'], 'paged' => $request['page'], 'geo_query' => array( 'lat_field' => 'flat_lat', // this is the name of the meta field storing latitude 'lng_field' => 'flat_lng', // this is the name of the meta field storing longitude 'latitude' => $lat1, // this is the latitude of the point we are getting distance from 'longitude' => $lng1, // this is the longitude of the point we are getting distance from 'distance' => $proximity, // this is the maximum distance to search 'units' => 'miles' // this supports options: miles, mi, kilometers, km ), 'meta_query' => array( ), 'orderby' => 'distance', // this tells WP Query to sort by distance 'order' => 'ASC' ); // print_r('sorted'); // die; } else { $args = array( 'update_post_term_cache' => false, 'post_type' => 'vandelay_industries', 'posts_per_page' => $request['per_page'], 'paged' => $request['page'], 'meta_query' => array( ) ); // print_r('not'); // die; } // use WP_Query to get the results with pagination $query = new WP_Query( $args ); // if no posts found return if( empty($query->posts) ){ return new WP_Error( 'no_posts', __('No post found'), array( 'status' => 404 ) ); } // set max number of pages and total num of posts $posts = $query->posts; $max_pages = $query->max_num_pages; $total = $query->found_posts; foreach ( $posts as $post ) { $response = $this->prepare_item_for_response( $post, $request ); $data[] = $this->prepare_response_for_collection( $response ); } // set headers and return response $response = new WP_REST_Response($data, 200); if($foundCity){ // $updatedAddress = $foundCity .', '.$foundState; $response->header( 'GEO-Address', $foundCity ); $response->header( 'GEO-Lat', $lat1 ); $response->header( 'GEO-Lng', $lng1 ); } $response->header( 'X-WP-Total', $total ); $response->header( 'X-WP-TotalPages', $max_pages ); // return data/response return $response; } /** * Check if a given request has access to post items. */ public function vandelay_industries_locations_permissions_check( $request ) { return true; } /** * Get the query params for collections */ public function get_collection_params() { return array( 'page' => array( 'description' => 'Current page of the collection.', 'type' => 'integer', 'default' => 1, 'sanitize_callback' => 'absint', ), 'per_page' => array( 'description' => 'Maximum number of items to be returned in result set.', 'type' => 'integer', 'default' => 6, 'sanitize_callback' => 'absint', ), ); } /** * Prepares post data for return as an object. */ public function prepare_item_for_response( $post, $request ) { // get card id $postID = $post->ID; // meta fields notes: /* * get_post_custom returned ALL META, we only want ACF * get_fields is a unique helper function to ACF (only gets Meta Fields done by ACF) * returns array of ALL ACF fields * if empty, should return null by default per field * * was previously using get_post_meta() for the fields individually * this would be the way to go if the cards had more meta fields that had nothing to do with the API data returned / example: custom page builder for the front-end */ $acfFields = get_fields($postID); // get card title - post title - default field $postTitle = $post->post_title; // link $permalink = get_permalink($postID); // featured image $featuredImage = get_the_post_thumbnail_url($postID,'medium-large'); $data = array( 'id' => $postID, 'title' => $postTitle, 'link' => $permalink, 'featured_image' => $featuredImage, ); return $data; } } add_action( 'rest_api_init', function () { $controller = new FANCY_SQUARES_MAP_RESULTS_API_ENDPOINT(); $controller->register_routes(); } );
And finally, and example AJAX call that would bring it all together.
// get variables from url to pass to api function setApiData(miles) { const miles = setMiles; const paramsPage = 1 const paramsPerPage = 6 return { location: 'address and/or locaitons goes here', proximity: miles, per_page: paramsPerPage, page: paramsPage, }; } function getCostanzas(setMiles) { axios.get('/wp-json/fancysquares/v1/vandelay_industries', { params: setApiData }) .then((response) => { // handle success, do what you want! console.log(response); }) .catch((error) => { // handle error console.log(error.message); }) .then(() => { // always executed }); } export { getCostanzas };
A special thanks to the following resources where I put this all together from ( for the radius and location filtering )
I encourage your to try both of the above links as just standard WP_Query
calls to get a better idea of what’s going on before implementing it with the REST API.