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 Google Cloud Console',
'Enable Geocoding API + Places API for frontend key, restricted by your server IP address',
'Enable Maps Platform API 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'];
}
}