<?php
|
/**
|
* Google Maps Integration
|
* File: /inc/integrations/GoogleMapsIntegration.php
|
*/
|
|
namespace JVBase\integrations\services;
|
use JVBase\integrations\Admin;
|
use JVBase\Instance;
|
use JVBase\integrations\Integrations;
|
use JVBase\integrations\Requests;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
class GoogleMaps extends Integrations
|
{
|
use Admin, Instance, Requests;
|
protected function __construct()
|
{
|
$this->service_name = 'maps';
|
$this->title = 'Google Maps';
|
$this->icon = 'map-pin';
|
|
$this->apiBase = 'https://maps.googleapis.com/maps/api/';
|
$this->defineFields();
|
$this->defineInstructions();
|
|
parent::__construct();
|
// Auto-enqueue maps script when needed
|
add_action('wp_enqueue_scripts', [$this, 'maybeEnqueueMapsScript']);
|
}
|
protected function defineFields():void
|
{
|
$pub = $this->addField('public_key', 'Maps Platform API Key', 'text', 'manage_options');
|
$pub->setHint('Restrict this key by HTTP referrers for security. Used for displaying maps in browsers.');
|
$pub->setSubType('password');
|
$pub->setRequired();
|
|
$priv = $this->addField('private_key', 'Geocoding & Places API Key', 'text', 'manage_options');
|
$priv->setHint('Restrict this key by IP Addresses. Used for geocoding and server-side operations.');
|
$priv->setRequired();
|
$priv->setSubType('password');
|
|
$map_id = $this->addField('map_id', 'Map ID', 'text', 'manage_options');
|
$map_id->setHint('Required for custom styling. Create a Map ID in Google Cloud Console -> Maps -> Map Management.');
|
|
$zoom = $this->addField('default_zoom', 'Default Zoom', 'text', 'manage_options');
|
$zoom->setSubType('number');
|
$zoom->setDefault(11);
|
$zoom->setMin(1);
|
$zoom->setMax(20);
|
|
$lng = $this->addField('default_lng', 'Default Longitude', 'text', 'manage_options');
|
$lng->setSubType('number');
|
$lng->setDefault(-113.4938);
|
|
$lat = $this->addField('default_lat', 'Default Latitude', 'text', 'manage_options');
|
$lng->setSubType('number');
|
$lng->setDefault(53.5461);
|
}
|
|
protected function defineInstructions():void
|
{
|
$this->instructions = [
|
'Create two API keys in <a href="https://console.cloud.google.com/apis/credentials" target="_blank">Google Cloud Console</a>',
|
'Enable <strong>Geocoding API + Places API</strong> for frontend key, restricted by your server IP address',
|
'Enable <strong>Maps Platform API</strong> for server key, restricted by your website domain',
|
];
|
}
|
|
protected function defineActions():void
|
{
|
$this->addAction(
|
'reverse_geocode',
|
[$this, 'processReverseGeocode']
|
);
|
|
$this->addAction(
|
'geocode',
|
[$this, 'processGeocode']
|
);
|
}
|
public function processReverseGeocode(array $data):array
|
{
|
if (!array_key_exists('lat', $data) || !array_key_exists('lng', $data)) {
|
return $this->response(false, 'Invalid coordinates provided', $data);
|
}
|
$lat = floatval($data['lat']);
|
$lng = floatval($data['lng']);
|
|
$result = $this->reverseGeocode($lat, $lng);
|
if ($result) {
|
return $this->response(true, 'Successfully reversed Geocode', ['data'=> $result]);
|
}
|
return $this->response(false, 'Reverse geocoding failed');
|
}
|
|
public function processGeocode(array $data):array
|
{
|
if (!array_key_exists('address', $data)) {
|
return $this->response(false, 'No address provided');
|
}
|
$result = $this->geocodeAddress(sanitize_text_field($data['address']));
|
if ($result) {
|
return $this->response(true, 'Successfully geocoded address', ['data' => $result]);
|
}
|
return $this->response(false, 'Geocoding failed');
|
}
|
protected function initialize(): void
|
{
|
// Register Google Maps API
|
wp_register_script(
|
'google-maps-api',
|
$this->getJavaScriptApiUrl(['places', 'geometry', 'marker']),
|
[],
|
null,
|
[
|
'in_footer' => true,
|
'strategy' => 'async'
|
]
|
);
|
|
// Register our maps handler
|
wp_register_script(
|
'jvb-maps',
|
JVB_URL . 'assets/js/min/maps.min.js',
|
['google-maps-api'],
|
'1.0.0',
|
[
|
'in_footer' => true,
|
'strategy' => 'defer'
|
]
|
);
|
}
|
public function getJavaScriptApiUrl(array $libraries = ['places', 'marker', 'geometry']): string
|
{
|
if (empty($this->getPublicKey())) {
|
return '';
|
}
|
|
$params = [
|
'key' => $this->getPublicKey(),
|
'libraries' => implode(',', $libraries),
|
'callback' => 'initGoogleMaps',
|
];
|
|
return $this->apiBase . 'js?' . http_build_query($params);
|
}
|
|
|
public function maybeEnqueueMapsScript():void
|
{
|
if (!$this->isSetUp()) {
|
return;
|
}
|
if (!$this->shouldEnqueueMaps()) {
|
return;
|
}
|
|
// Add initialization script
|
wp_add_inline_script('google-maps-api',
|
sprintf(
|
'
|
// Set map defaults
|
window.jvbMapDefaults = {
|
lat: %s,
|
lng: %s,
|
zoom: %s,
|
mapId: "%s"
|
};
|
|
// Google Maps initialization callback
|
window.initGoogleMaps = function() {
|
// Dispatch ready event
|
document.dispatchEvent(new Event("googleMapsReady"));
|
|
// Call any registered callbacks
|
if (window.googleMapsCallbacks && Array.isArray(window.googleMapsCallbacks)) {
|
window.googleMapsCallbacks.forEach(function(callback) {
|
if (typeof callback === "function") {
|
callback();
|
}
|
});
|
window.googleMapsCallbacks = [];
|
}
|
};
|
|
// Initialize callback queue
|
window.googleMapsCallbacks = window.googleMapsCallbacks || [];
|
',
|
$this->getDefaultLat(),
|
$this->getDefaultLng(),
|
$this->getDefaultZoom(),
|
esc_js($this->getMapID())
|
),
|
'before');
|
|
// Enqueue scripts
|
wp_enqueue_script('google-maps-api');
|
wp_enqueue_script('jvb-maps');
|
|
// Add AJAX URL and nonce for JavaScript AJAX calls
|
wp_localize_script('jvb-maps', 'jvbMapsAjax', [
|
'ajaxurl' => admin_url('admin-ajax.php'),
|
'nonce' => wp_create_nonce('jvb_integration_' . $this->service_name),
|
'service' => $this->service_name
|
]);
|
}
|
protected function shouldEnqueueMaps():bool
|
{
|
//See if it's a singular post/page that has the map feature flagged
|
if (is_singular() && get_post_meta(get_the_ID(), BASE.'has_map', true) === true) {
|
return true;
|
}
|
if (is_tax() && get_term_meta(get_queried_object_id(), BASE . 'has_map', true) === true) {
|
return true;
|
}
|
|
// Allow filtering
|
return apply_filters('jvb_should_enqueue_maps', false);
|
}
|
|
public function getServiceDescription():string
|
{
|
return 'Allows for a map field, utilizing Google Maps';
|
}
|
/**********************************************************************
|
* UTILITY
|
**********************************************************************/
|
protected function getPrivateKey():string|false
|
{
|
return $this->loadCredentials()['private_key']??false;
|
}
|
protected function getPublicKey():string|false
|
{
|
return $this->loadCredentials()['public_key']??false;
|
}
|
|
protected function getMapID():string|false
|
{
|
return $this->loadCredentials()['map_id']??false;
|
}
|
protected function getDefaultZoom():string|false
|
{
|
return $this->loadCredentials()['default_zoom']??false;
|
}
|
protected function getDefaultLng():string|false
|
{
|
return $this->loadCredentials()['default_lng']??false;
|
}
|
protected function getDefaultLat():string|false
|
{
|
return $this->loadCredentials()['default_lat']??false;
|
}
|
|
|
/**
|
* Generate map application links
|
*/
|
public function generateMapLinks(float $lat, float $lng, ?string $address = null): array
|
{
|
$query = $address ? urlencode($address) : "{$lat},{$lng}";
|
|
return [
|
'google' => "https://www.google.com/maps/search/?api=1&query={$query}",
|
'apple' => "https://maps.apple.com/?ll={$lat},{$lng}",
|
'waze' => "https://waze.com/ul?ll={$lat},{$lng}&navigate=yes"
|
];
|
}
|
private function parseAddressComponents(array $components):array
|
{
|
$parsed = [];
|
$check = [
|
'street_number' => 'long_name',
|
'street' => 'long_name',
|
'city' => 'long_name',
|
'province' => 'short_name',
|
'country' => 'short_name',
|
'postal_code' => 'long_name'
|
];
|
foreach ($components as $c) {
|
$types = $c['types'];
|
foreach ($check as $ch => $index) {
|
if (in_array($ch, $types)) {
|
$parsed[$ch] = $c[$index];
|
}
|
}
|
}
|
if (!empty($parsed['street_number']) && !empty($parsed['street'])) {
|
$parsed['street'] = $parsed['street_number'] . ' ' . $parsed['street'];
|
}
|
return $parsed;
|
}
|
|
public function reverseGeocode(float $lat, float $lng):array|false
|
{
|
if (!$this->isSetUp()) {
|
return false;
|
}
|
$params = [
|
'latlng' => "{$lat},{$lng}",
|
'key' => $this->getPrivateKey()
|
];
|
return $this->getRequest(
|
sprintf(
|
'%s/geocode/json',
|
$this->apiBase,
|
),
|
$params
|
);
|
}
|
protected function handleGeocodeResponse(array $data, string $method):array
|
{
|
if (!array_key_exists('status', $data) || $data['status'] !== 'OK' || !array_key_exists('results', $data) || empty($data['results'])) {
|
return $this->response(false, 'Invalid response provided');
|
}
|
$result = $data['results'][0];
|
return $this->response(true, 'Successfully reversed geocode',[
|
'lat' => $result['geometry']['location']['lat'],
|
'lng' => $result['geometry']['location']['lng'],
|
'formatted_address' => $result['formatted_address'],
|
'place_id' => $result['place_id']??'',
|
'components' => $this->parseAddressComponents($result['address_compenents']??[])
|
]);
|
}
|
|
public function geocodeAddress(string $address):array|false
|
{
|
if (!$this->isSetUp()) {
|
return false;
|
}
|
$params = [
|
'address' => $address,
|
'key' => $this->getPrivateKey()
|
];
|
return $this->getRequest(
|
sprintf(
|
'%s/geocode/json',
|
$this->apiBase
|
)
|
);
|
}
|
|
protected function getRequestHeaders(): array
|
{
|
return [
|
'Accept' => 'application/json',
|
'Content-Type' => 'application/json'
|
];
|
}
|
|
protected function handleResponse(\WP_Error|array $response, string $method, string $endpoint): array
|
{
|
if (is_wp_error($response) && $response->get_error_message() === 'rate_limit_exceeded') {
|
return $this->response(false, 'Rate limit exceeded');
|
}
|
$endpoint = str_replace($this->apiBase, '', $endpoint);
|
return match ($endpoint) {
|
'/geocode/json' => $this->handleGeocodeResponse($response, $method),
|
default => $this->response(false, 'Unhandled response for '.$endpoint.' endpoint, with method: '.$method),
|
};
|
}
|
|
protected function performConnectionTest():bool
|
{
|
if (!$this->isSetUp()) {
|
return false;
|
}
|
|
$test = 'Edmonton, Alberta, Canada';
|
$result = $this->geocodeAddress($test);
|
return $result['success'];
|
}
|
}
|