Skip to content

Instantly share code, notes, and snippets.

@owaisahmed5300
Last active April 29, 2024 11:31
Show Gist options
  • Save owaisahmed5300/242181f40cfcb60716d73d2f6941fa3c to your computer and use it in GitHub Desktop.
Save owaisahmed5300/242181f40cfcb60716d73d2f6941fa3c to your computer and use it in GitHub Desktop.
WordPress WP Query GEO Query Support and Google Map AdvancedMarkerElement Example
<?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();
<?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
)
);
<?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.
),
)
),
)
);
<!--
(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