Last active
April 29, 2024 11:31
-
-
Save owaisahmed5300/242181f40cfcb60716d73d2f6941fa3c to your computer and use it in GitHub Desktop.
WordPress WP Query GEO Query Support and Google Map AdvancedMarkerElement Example
This file contains hidden or 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 | |
/** | |
* Geo Query | |
* | |
* This class provides methods to perform geographical queries in WordPress, | |
* allowing users to filter posts based on geographical parameters like distance. | |
* | |
* @package WPSnippets | |
* @author Owaisansari53 <[email protected]> | |
*/ | |
namespace WPSnippets; // you may change this. | |
use WP_Post; | |
use WP_Query; | |
defined('ABSPATH') || exit; | |
/** | |
* Geo Query | |
* | |
* This class provides methods to perform geographical queries in WordPress. | |
* | |
* @package WPSnippets | |
* @author Owaisansari53 <[email protected]> | |
*/ | |
class GEO_Query | |
{ | |
/** | |
* Singleton instance of the GEO_Query class. | |
* | |
* @var GEO_Query The singleton instance. | |
*/ | |
private static $instance; | |
/** | |
* Retrieves the singleton instance of the GEO_Query class. | |
* | |
* @return GEO_Query The singleton instance of the GEO_Query class. | |
*/ | |
public static function get_instance(): Geo_Query | |
{ | |
if (!self::$instance) { | |
self::$instance = new self(); | |
} | |
return self::$instance; | |
} | |
/** | |
* Constructs the GEO_Query object. | |
* Registers necessary WordPress hooks for performing geographical queries. | |
*/ | |
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); | |
} | |
/** | |
* Adds a calculated "distance" parameter to the SQL query using a haversine formula. | |
* | |
* @param string|null $sql The SQL query string. | |
* @param WP_Query $query The WP_Query object. | |
* | |
* @return string The modified SQL query string. | |
*/ | |
public function posts_fields(?string $sql, WP_Query $query): ?string | |
{ | |
$geo_query = $query->get('geo_query'); | |
if ($geo_query) { | |
if ($sql) { | |
$sql .= ', '; | |
} | |
$sql .= $this->haversine_term($geo_query) . " AS geo_query_distance"; | |
} | |
return $sql; | |
} | |
/** | |
* Modifies the SQL query to include necessary joins for geographical queries. | |
* | |
* @param string|null $sql The SQL query string. | |
* @param WP_Query $query The WP_Query object. | |
* | |
* @return string The modified SQL query string. | |
*/ | |
public function posts_join(?string $sql, WP_Query $query): ?string | |
{ | |
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; | |
} | |
/** | |
* Modifies the SQL query to include conditions for geographical queries. | |
* | |
* @param string|null $sql The SQL query string. | |
* @param WP_Query $query The WP_Query object. | |
* | |
* @return string|null The modified SQL query string. | |
*/ | |
public function posts_where(?string $sql, WP_Query &$query): ?string | |
{ | |
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']; | |
} | |
$lng_field = 'longitude'; | |
if (!empty($geo_query['lng_field'])) { | |
$lng_field = $geo_query['lng_field']; | |
} | |
if ($sql) { | |
$sql .= " AND "; | |
} | |
if (isset($geo_query['bounds'])) { | |
$sql .= $wpdb->prepare( | |
"(geo_query_lat.meta_key = %s AND geo_query_lng.meta_key = %s AND | |
geo_query_lat.meta_value BETWEEN %f AND %f AND | |
geo_query_lng.meta_value BETWEEN %f AND %f)", | |
$lat_field, | |
$lng_field, | |
$geo_query['bounds']['southwest']['lat'], | |
$geo_query['bounds']['northeast']['lat'], | |
$geo_query['bounds']['southwest']['lng'], | |
$geo_query['bounds']['northeast']['lng'] | |
); | |
} else { | |
$distance = 20; // Default distance | |
if (isset($geo_query['distance'])) { | |
$distance = $geo_query['distance']; | |
} | |
$haversine = $this->haversine_term($geo_query); | |
$sql .= $wpdb->prepare( | |
"(geo_query_lat.meta_key = %s AND geo_query_lng.meta_key = %s AND " . $haversine . " <= %f )", | |
$lat_field, | |
$lng_field, | |
$distance | |
); | |
} | |
} | |
return $sql; | |
} | |
/** | |
* Modifies the SQL query to handle ordering based on geographical distance. | |
* | |
* @param string|null $sql The SQL query string. | |
* @param WP_Query $query The WP_Query object. | |
* | |
* @return string|null The modified SQL query string. | |
*/ | |
public function posts_orderby(?string $sql, WP_Query $query): ?string | |
{ | |
$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; | |
} | |
/** | |
* Retrieves the distance from a given post to the location provided in WP Query. | |
* | |
* @param WP_Post|int|null $post_id_or_obj The post object or ID. Defaults to the global $post. | |
* @param bool $round Whether to round the distance or not. | |
*/ | |
public static function the_distance($post_id_or_obj = null, bool $round = false) | |
{ | |
echo self::get_the_distance($post_id_or_obj, $round); | |
} | |
/** | |
* Retrieves the distance from a given post to the location provided in WP Query. | |
* | |
* @param WP_Post|int|null $post_id_or_obj The post object or ID. Defaults to the global $post. | |
* @param bool $round Whether to round the distance or not. | |
* | |
* @return float|bool The distance from the post to the current user's location, or false if not available. | |
*/ | |
public static function get_the_distance($post_id_or_obj = null, bool $round = false) | |
{ | |
global $post; | |
if (!$post_id_or_obj) { | |
$post_id_or_obj = $post; | |
} | |
$post_id_or_obj = is_numeric($post_id_or_obj) ? get_post($post_id_or_obj) : $post_id_or_obj; | |
if (property_exists($post_id_or_obj, 'geo_query_distance')) { | |
$distance = $post_id_or_obj->geo_query_distance; | |
if ($round !== false) { | |
$distance = round($distance, $round); | |
} | |
return $distance; | |
} | |
return false; | |
} | |
/** | |
* Calculates the haversine term for the given geographical query parameters. | |
* | |
* @param array $geo_query The geographical query parameters. | |
* | |
* @return string The haversine term for the given parameters. | |
*/ | |
private function haversine_term(array $geo_query): string | |
{ | |
global $wpdb; | |
$units = "miles"; | |
if (!empty($geo_query['units'])) { | |
$units = strtolower($geo_query['units']); | |
} | |
$radius = 3959; // Default radius in miles | |
if (in_array($units, array('km', 'kilometers'), true)) { | |
$radius = 6371; // Radius in kilometers | |
} | |
$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 .= ")"; | |
return $wpdb->prepare($haversine, array($lat, $lng, $lat)); | |
} | |
} | |
// Initialize the GEO_Query instance | |
GEO_Query::get_instance(); |
This file contains hidden or 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 | |
// (if need help): | |
// https://www.fiverr.com/owaisansari53 | |
// [email protected] | |
// Below WP Query demonstrate to fetch listings of New York City Center with distance of 30 miles. | |
$query = new WP_Query( | |
array( | |
'post_type' => 'YOUR_POST_TYPE', // i.e. listings | |
'post_status' => 'publish', | |
'posts_per_page' => -1, | |
// Meta query to make sure lat/lng exists. | |
'meta_query' => array( | |
'relation' => 'AND', | |
array( | |
'key' => 'LATITUDE_META_KEY', // change this. | |
'compare' => 'EXISTS', | |
), | |
array( | |
'key' => 'LONGITUDE_META_KEY', // change this. | |
'compare' => 'EXISTS', | |
) | |
), | |
// Geo Query | |
'geo_query' => array( | |
'lat_field' => 'LATITUDE_META_KEY', // change this. | |
'lng_field' => 'LONGITUDE_META_KEY', // change this. | |
'latitude' => 40.7580, // New york center. | |
'longitude' => 73.9855, // New york center. | |
'distance' => 30, | |
'units' => 'miles' // accepts km, kilometers, mi, miles. | |
), | |
// Sorting. | |
'orderby' => 'distance', | |
'order' => 'ASC' // ASC=Close, DESC=Far | |
) | |
); |
This file contains hidden or 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 | |
// (if need help): | |
// https://www.fiverr.com/owaisansari53 | |
// [email protected] | |
// Below is a WP Query demonstrating how to fetch listings within certain geographical boundaries | |
$query = new WP_Query( | |
array( | |
'post_type' => 'YOUR_POST_TYPE', // i.e. listings | |
'post_status' => 'publish', | |
'posts_per_page' => -1, | |
// Meta query to make sure lat/lng exists. | |
'meta_query' => array( | |
'relation' => 'AND', | |
array( | |
'key' => 'LATITUDE_META_KEY', // change this. | |
'compare' => 'EXISTS', | |
), | |
array( | |
'key' => 'LONGITUDE_META_KEY', // change this. | |
'compare' => 'EXISTS', | |
) | |
), | |
// Geo Query | |
'geo_query' => array( | |
'lat_field' => 'LATITUDE_META_KEY', // change this. | |
'lng_field' => 'LONGITUDE_META_KEY', // change this. | |
'bounds' => array( | |
'southwest' => array( | |
'lat' => '', // you may get this from google map via ajax. | |
'lng' => '', // you may get this from google map via ajax. | |
), | |
'northeast' => array( | |
'lat' => '', // you may get this from google map via ajax. | |
'lng' => '', // you may get this from google map via ajax. | |
), | |
) | |
), | |
) | |
); |
This file contains hidden or 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
<!-- | |
(if need help): | |
https://www.fiverr.com/owaisansari53 | |
[email protected] | |
Below is a WP Query demonstrating how to fetch listings within certain geographical boundaries | |
--> | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Google Maps AJAX Example</title> | |
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places,marker" async defer></script> | |
<script> | |
function initMap() { | |
var map; | |
var markers = []; | |
var map = new google.maps.Map(document.getElementById('map'), { | |
center: {lat: 0, lng: 0}, // Ofcourse, you need to change this and configure it for your needs. | |
zoom: 8, | |
mapId: 'any_map_id' // This is not html element id but a unique identifier for this map i.e. myplugin_listings_map. | |
}); | |
map.addListener('bounds_changed', function() { | |
var bounds = map.getBounds(); | |
var ne = bounds.getNorthEast(); | |
var sw = bounds.getSouthWest(); | |
var data = { | |
action: 'myplugin_fetch_listings', | |
northeast_lat: ne.lat(), | |
northeast_lng: ne.lng(), | |
southwest_lat: sw.lat(), | |
southwest_lng: sw.lng() | |
}; | |
// Make an AJAX request to fetch listings | |
// You may need to adjust the URL based on your setup | |
jQuery.post(ajaxurl, data, function(response) { | |
// Clear existing markers | |
markers.forEach(function(marker) { | |
marker.setMap(null); | |
}); | |
// Parse the response JSON | |
var listings = JSON.parse(response); | |
// Register new markers | |
listings.forEach(function(listing) { | |
var marker = new google.maps.marker.AdvancedMarkerElement({ | |
position: {lat: parseFloat(listing.latitude), lng: parseFloat(listing.longitude)}, | |
map: map, | |
title: listing.title | |
}); | |
markers.push(marker); | |
}); | |
}); | |
}); | |
} | |
window.addEventListener('load', initMap); | |
</script> | |
</head> | |
<body> | |
<div id="map" style="height: 700px; width: 100%;"></div> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment