Source: my-calendar-geolocation.php

<?php
/**
 * Geolocation functions.
 *
 * @category Features
 * @package  My Calendar Pro
 * @author   Joe Dolson
 * @license  GPLv3
 * @link     https://www.joedolson.com/my-calendar-pro/
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

require __DIR__ . '/vendor/autoload.php';

use GeoIp2\Database\Reader;

/**
 * Get user location by IP.
 */
function mcs_get_user_ip() {
	// disabled by default, as required database is proprietary.
	if ( ! ( defined( 'MCS_GEOLOCATION_ENABLED' ) && MCS_GEOLOCATION_ENABLED ) ) {
		return false;
	}

	$client  = isset( $_SERVER['HTTP_CLIENT_IP'] ) ? sanitize_text_field( $_SERVER['HTTP_CLIENT_IP'] ) : '';
	$forward = isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ? sanitize_text_field( $_SERVER['HTTP_X_FORWARDED_FOR'] ) : '';
	$remote  = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( $_SERVER['REMOTE_ADDR'] ) : '';

	if ( filter_var( $client, FILTER_VALIDATE_IP ) ) {
		$ip = $client;
	} elseif ( filter_var( $forward, FILTER_VALIDATE_IP ) ) {
		$ip = $forward;
	} else {
		$ip = $remote;
	}

	return $ip;
}

/**
 * Submit a query to recenter geolocation map.
 *
 * @param array $address Address information.
 *
 * @return string
 */
function mcs_geolocate_form( $address = array() ) {
	$nonce = wp_create_nonce( 'mc-geolocate-nonce' );
	// Enqueue Maps scripting.
	$api_key = mc_get_option( 'gmap_api_key' );
	if ( $api_key ) {
		$args = array(
			'strategy' => 'async',
		);
		wp_enqueue_script( 'mc-maps' );
		wp_localize_script(
			'mc-maps',
			'gmaps',
			array(
				'toggle' => '<span class="dashicons dashicons-arrow-right" aria-hidden="true"></span><span class="screen-reader-text">' . __( 'Location Details', 'my-calendar-pro' ) . '</span>',
			)
		);
	}
	$fields = array(
		'street' => array(
			'label' => __( 'Street Address', 'my-calendar-pro' ),
			'auto'  => 'address-line1',
		),
		'city'   => array(
			'label' => __( 'City', 'my-calendar-pro' ),
			'auto'  => 'address-level2',
		),
		'state'  => array(
			'label' => __( 'State or Province', 'my-calendar-pro' ),
			'auto'  => 'address-level1',
		),
		'zip'    => array(
			'label' => __( 'Postal Code', 'my-calendar-pro' ),
			'auto'  => 'postal-code',
		),
	);
	$fields = apply_filters( 'mc_geolocate_fields', $fields );
	if ( empty( $fields ) ) {
		return '';
	}
	$output = '';
	foreach ( $fields as $key => $value ) {
		$current = ( ! empty( $address ) && isset( $address[ $key ] ) ) ? $address[ $key ] : '';
		if ( isset( $_REQUEST['mc_address'] ) ) {
			$address = map_deep( $_REQUEST['mc_address'], 'sanitize_text_field' );
			$current = isset( $address[ $key ] ) ? $address[ $key ] : '';
		}
		$output .= '<p class="mc_field_' . sanitize_html_class( $key ) . '">
			<label for="mc_id_' . sanitize_html_class( $key ) . '">' . esc_html( $value['label'] ) . '</label>
			<input type="text" autocomplete="' . esc_attr( $value['auto'] ) . '" value="' . $current . '" id="mc_id_' . sanitize_html_class( $key ) . '" name="mc_address[' . sanitize_html_class( $key ) . ']" />
		</p>';
	}
	$form = '
	<div class="mc-geolocate-form">
		<h2 class="screen-reader-text">' . esc_html__( 'Search Locations', 'my-calendar-pro' ) . '</h2>
		<form action="" method="POST">
			<input type="hidden" name="mc_gl_nonce" value="' . $nonce . '" />
			<div class="mc-gl-fields">
				' . $output . '
				<p class="mc-submit-button"><button class="button-primary mc-button">' . esc_html__( 'Find Nearby Locations', 'my-calendar-pro' ) . '</button></p>
			</div>
		</form>
	</div>';

	return $form;
}

/**
 * Get locations proximate to center point.
 *
 * @param float $latitude Center latitude.
 * @param float $longitude Center longitude.
 * @param array $address Array of address strings.
 * @param int   $radius Radius to search in $unit.
 * @param int   $limit Number of results to display.
 * @param int   $unit Unit for the `$radius` parameter (miles or kilometers).
 *
 * @return array
 */
function mcs_geolocate( $latitude = false, $longitude = false, $address = array(), $radius = 100, $limit = 25, $unit = 'kilometers' ) {
	global $wpdb;
	$form   = '';
	$output = '';
	$text   = '';

	if ( isset( $_POST['mc_gl_nonce'] ) ) {
		$security = wp_verify_nonce( $_POST['mc_gl_nonce'], 'mc-geolocate-nonce' );
		if ( $security ) {
			$address = isset( $_REQUEST['mc_address'] ) ? map_deep( $_REQUEST['mc_address'], 'sanitize_text_field' ) : $address;
		}
	}
	if ( ! empty( $address ) ) {
		$coordinates = mc_get_location_coordinates( false, $address );
		$latitude    = isset( $coordinates['latitude'] ) ? $coordinates['latitude'] : false;
		$longitude   = isset( $coordinates['longitude'] ) ? $coordinates['longitude'] : false;
	}
	if ( ! $latitude || ! $longitude ) {
		$ip = mcs_get_user_ip();
		if ( $ip ) {
			$filepath = plugin_dir_path( __DIR__ ) . 'my-calendar-custom/db/GeoLite2-City.mmdb';
			/**
			 * Filter the location of the GeoLite City database.
			 *
			 * @hook mcs_geolocate_db_filepath
			 *
			 * @param {string} $filepath Path to file.
			 *
			 * @return {string}
			 */
			$filepath = apply_filters( 'mcs_geolocate_db_filepath', $filepath );
			if ( file_exists( $filepath ) ) {
				$reader = new Reader( $filepath );
				try {
					$response  = $reader->city( $ip );
					$latitude  = (float) $response->location->latitude;
					$longitude = (float) $response->location->longitude;
				} catch ( Exception $e ) {
					if ( $e->getMessage() && current_user_can( 'manage_options' ) ) {
						$text = $e->getMessage();
					}
				}
			}
		} else {
			$latitude  = 0.000000;
			$longitude = 0.000000;
		}
	}
	$display_radius = (int) $radius;

	$radius = ( 'kilometers' === $unit ) ? (int) $radius : (int) ( $radius / .62 );
	$limit  = (int) $limit;

	// If units = miles, 3956 & 69; if km = 6371 & 111.0447
	// since MySQL 5.7.6; MariaDB 10.2.38; 10.3.29; 10.4.19; 10.5.10;
	// When ST Distance Sphere supported; .00062... = miles; .001 = km; default is meters.
	$locations = $wpdb->get_results(
		$wpdb->prepare(
			'SELECT *, ST_Distance_Sphere( point( %f, %f ), point(location_longitude, location_latitude)) * .000621371192 AS distance_in_miles FROM %i HAVING distance_in_miles <= %f ORDER BY distance_in_miles ASC LIMIT %d',
			$longitude,
			$latitude,
			my_calendar_locations_table(),
			$radius,
			$limit
		)
	);
	if ( empty( $locations ) ) {
		// Alternate query if ST Distance Sphere not supported.
		$locations = $wpdb->get_results(
			$wpdb->prepare(
				'SELECT *, 3956 * 2 * ASIN(SQRT( POWER(SIN((%f - location_latitude)*pi()/180/2),2)
				+COS(%f*pi()/180 )*COS(location_latitude*pi()/180)*POWER(SIN((%f-location_longitude)*pi()/180/2),2)
				)) AS distance FROM %i WHERE location_longitude BETWEEN (%f-%f/cos(radians(%f))*69)
				AND (%f+%f/cos(radians(%f))*69) AND location_latitude BETWEEN (%f-(%f/69))
				AND (%f+(%f/69)) HAVING distance < %f ORDER BY distance LIMIT %d',
				$latitude,
				$latitude,
				$longitude,
				my_calendar_locations_table(),
				$longitude,
				$radius,
				$latitude,
				$longitude,
				$radius,
				$latitude,
				$latitude,
				$radius,
				$latitude,
				$radius,
				$radius,
				$limit
			)
		);
	}
	if ( ! empty( $locations ) ) {
		// Add location post ID to object.
		foreach ( $locations as $key => $location ) {
			$location->location_post = mc_get_location_post( $location->location_id, false );
			$locations[ $key ]       = $location;
		}
		$form     = mcs_geolocate_form( $address );
		$count    = count( $locations );
		$distance = $display_radius;
		// translators: 1) number of locations, 2) distance away, 3) units (miles or kilometers).
		$text   = '<p class="mcs-geolocation-notice">' . esc_html( sprintf( _n( '%1$d location found within %2$d %3$s.', '%1$d locations found within %2$d %3$s.', $count, 'my-calendar-pro' ), $count, $distance, $unit ) ) . '</p>';
		$output = mc_generate_map( $locations, 'location', true, true );
	}

	return '<div class="mc-geolocate-wrapper">' . $form . $text . $output . '</div>';
}
add_shortcode( 'geolocate', 'mcs_geolocation_map' );

/**
 * Generate a geolocation map based on latitude/longitude.
 *
 * @param array  $atts Shortcode attributes.
 * @param string $content Wrapped shortcode content.
 *
 * @return string
 */
function mcs_geolocation_map( $atts, $content ) {
	$args = shortcode_atts(
		array(
			'latitude'  => '',
			'longitude' => '',
			'radius'    => 100,
			'limit'     => 25,
			'unit'      => 'kilometers', // or miles.
		),
		$atts,
		'geolocate'
	);

	return mcs_geolocate( $args['latitude'], $args['longitude'], array(), $args['radius'], $args['limit'], $args['unit'] );
}