service_name = 'maps';
$this->title = 'Google Maps';
$this->icon = 'map-pin';
$this->apiBase = 'https://maps.googleapis.com/maps/api/';
$this->apiEndpoints = [
'geocode/json',
'place/details/json',
'place/nearbysearch/json',
'distancematrix/json',
'js'
];
$this->fields = [
'public_key' => [
'type' => 'text',
'subtype' => 'password',
'label' => 'Maps Platform API Key (Referrer-Restricted)',
'hint' => 'Restrict this key by HTTP referrers for security. Used for displaying maps in browsers.',
'required' => true,
],
'private_key' => [
'type' => 'text',
'subtype' => 'password',
'label' => 'Geocoding & Places API Key (IP-Restricted)',
'hint' => 'Restrict this key by
IP Addresses. Used for geocoding and server-side operations.',
'required' => true,
]
];
$this->advanced = [
'map_id' => [
'type' => 'text',
'label' => 'Map ID',
'hint' => 'Required for custom styling. Create a Map ID in Google Cloud Console → Maps → Map Management'
],
'default_zoom' => [
'type' => 'text',
'subtype' => 'number',
'min' => 1,
'max' => 20,
'label' => 'Default Zoom',
'default' => 11
],
];
$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',
];
$this->defaults = [
];
parent::__construct();
$this->actions = array_merge(
$this->actions,
[
'reverse_geocode' => 'processReverseGeocode',
'geocode' => 'processGeocode'
]
);
// Auto-enqueue maps script when needed
add_action('wp_enqueue_scripts', [$this, 'maybeEnqueueMapsScript']);
// add_action('admin_enqueue_scripts', [$this, 'maybeEnqueueMapsScript']);
}
protected function initialize(): void
{
$this->private_key = $this->credentials['private_key'] ?? '';
$this->public_key = $this->credentials['public_key'] ?? '';
$this->map_id = $this->credentials['map_id'] ?? self::DEFAULT_MAP_ID;
$this->rate_limits = ['min_interval' => 0]; // Google Maps has generous limits
}
/**
* Get service description
*/
public function getServiceDescription(): string
{
return "Enable location features and embedded maps throughout your site.";
}
/**
* Get request headers (required by base class)
*/
protected function getRequestHeaders(): array
{
// Google Maps API uses API key in URL params, not headers
return [
'Accept' => 'application/json',
'Content-Type' => 'application/json'
];
}
/**
* Process test connection request
*/
protected function performConnectionTest(): bool
{
if (!$this->isSetUp()) {
return false;
}
// Test with a simple geocoding request
$test_address = 'Edmonton, Alberta, Canada';
$result = $this->geocodeAddress($test_address);
return $result !== null;
}
/**
* Process reverse geocode request
*/
private function processReverseGeocode(array $data): WP_Error|array
{
$lat = floatval($data['lat'] ?? 0);
$lng = floatval($data['lng'] ?? 0);
if (!$lat || !$lng) {
return new WP_Error('invalid_coordinates', 'Invalid coordinates provided');
}
$result = $this->reverseGeocode($lat, $lng);
if ($result) {
return ['success' => true, 'data' => $result];
}
return new WP_Error('geocoding_failed', 'Reverse geocoding failed');
}
/**
* Process geocode request
*/
private function processGeocode(array $data): WP_Error|array
{
$address = sanitize_text_field($data['address'] ?? '');
if (empty($address)) {
return new WP_Error('invalid_address', 'No address provided');
}
$result = $this->geocodeAddress($address);
if ($result) {
return ['success' => true, 'data' => $result];
}
return new WP_Error('geocoding_failed', 'Geocoding failed');
}
/**
* Geocode an address to coordinates
*/
public function geocodeAddress(string $address): ?array
{
if (!$this->isSetUp()) {
return null;
}
$params = [
'address' => $address,
'key' => $this->private_key
];
$key = $this->cache->generateKey(['geocode', $address]);
$cached = $this->cache->get($key);
if ($cached) {
return $cached;
}
$url = $this->apiBase . 'geocode/json?' . http_build_query($params);
$response = wp_remote_get($url);
if (is_wp_error($response)) {
$this->logError('Geocoding request failed', [
'error' => $response->get_error_message(),
'address' => $address
]);
return null;
}
$data = json_decode(wp_remote_retrieve_body($response), true);
if ($data['status'] === 'OK' && !empty($data['results'])) {
$result = $data['results'][0];
$result = [
'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_components'] ?? [])
];
$this->cache->set($key, $result);
} else {
$this->logError('Google Maps API error', [
'status' => $data['status'] ?? 'unknown',
'address' => $address
], 'warning');
$result = null;
}
return $result;
}
/**
* Reverse geocode coordinates to address
*/
public function reverseGeocode(float $lat, float $lng): ?array
{
if (!$this->isSetUp()) {
return null;
}
$params = [
'latlng' => "{$lat},{$lng}",
'key' => $this->private_key
];
$key = $this->cache->generateKey(['reverse', $lat, $lng]);
$cached = $this->cache->get($key);
if ($cached) {
return $cached;
}
$url = $this->apiBase . 'geocode/json?' . http_build_query($params);
$response = wp_remote_get($url);
if (is_wp_error($response)) {
$this->logError('Reverse geocoding request failed', [
'error' => $response->get_error_message(),
'lat' => $lat,
'lng' => $lng
]);
return null;
}
$data = json_decode(wp_remote_retrieve_body($response), true);
if ($data['status'] === 'OK' && !empty($data['results'])) {
$result = $data['results'][0];
$return = [
'formatted_address' => $result['formatted_address'],
'place_id' => $result['place_id'] ?? '',
'components' => $this->parseAddressComponents($result['address_components'] ?? [])
];
$this->cache->set($key, $return);
return $return;
}
return null;
}
/**
* Get place details by place ID
*/
public function getPlaceDetails(string $place_id, array $fields = []): ?array
{
if (!$this->isSetUp()) {
return null;
}
$default_fields = ['name', 'formatted_address', 'geometry', 'rating', 'formatted_phone_number'];
$fields = empty($fields) ? $default_fields : $fields;
$key = $this->cache->generateKey(['place', $place_id, $fields]);
$cached = $this->cache->get($key);
if ($cached) {
return $cached;
}
$params = [
'place_id' => $place_id,
'fields' => implode(',', $fields),
'key' => $this->private_key
];
$url = $this->apiBase . 'place/details/json?' . http_build_query($params);
$response = wp_remote_get($url);
if (is_wp_error($response)) {
$this->logError('Place details request failed', [
'error' => $response->get_error_message(),
'place_id' => $place_id
]);
return null;
}
$data = json_decode(wp_remote_retrieve_body($response), true);
if ($data['status'] === 'OK' && !empty($data['result'])) {
$result = $data['result'];
$this->cache->set($key, $result);
return $result;
}
return null;
}
/**
* Search for places nearby
*/
public function searchNearby(float $lat, float $lng, int $radius = 1000, string $type = ''): array
{
if (!$this->isSetUp()) {
return [];
}
$params = [
'location' => "{$lat},{$lng}",
'radius' => $radius,
'key' => $this->private_key
];
if (!empty($type)) {
$params['type'] = $type;
}
$key = $this->cache->generateKey(['nearby', $lat, $lng, $radius, $type]);
$cached = $this->cache->get($key);
if ($cached) {
return $cached;
}
$url = $this->apiBase . 'place/nearbysearch/json?' . http_build_query($params);
$response = wp_remote_get($url);
if (is_wp_error($response)) {
$this->logError('Nearby search request failed', [
'error' => $response->get_error_message(),
'lat' => $lat,
'lng' => $lng,
'radius' => $radius
]);
return [];
}
$data = json_decode(wp_remote_retrieve_body($response), true);
if ($data['status'] === 'OK' && !empty($data['results'])) {
$return = $data['results'];
$this->cache->set($key, $return);
return $return;
}
return [];
}
/**
* Calculate distance between two points
*/
public function calculateDistance(array $origin, array $destination, string $mode = 'driving'): ?array
{
if (!$this->isSetUp()) {
return null;
}
// Handle different input formats
$origin_str = is_array($origin) ? "{$origin['lat']},{$origin['lng']}" : $origin;
$dest_str = is_array($destination) ? "{$destination['lat']},{$destination['lng']}" : $destination;
$params = [
'origins' => $origin_str,
'destinations' => $dest_str,
'mode' => $mode,
'units' => 'metric',
'key' => $this->private_key
];
$key = $this->cache->generateKey(['distance', $origin_str, $dest_str, $mode]);
$cached = $this->cache->get($key);
if ($cached) {
return $cached;
}
$url = $this->apiBase . 'distancematrix/json?' . http_build_query($params);
$response = wp_remote_get($url);
if (is_wp_error($response)) {
$this->logError('Distance calculation request failed', [
'error' => $response->get_error_message(),
'origin' => $origin_str,
'destination' => $dest_str
]);
return null;
}
$data = json_decode(wp_remote_retrieve_body($response), true);
if ($data['status'] === 'OK' && !empty($data['rows'][0]['elements'][0])) {
$element = $data['rows'][0]['elements'][0];
if ($element['status'] === 'OK') {
$return = [
'distance' => $element['distance'],
'duration' => $element['duration'],
'mode' => $mode
];
$this->cache->set($key, $return);
return $return;
}
}
return null;
}
/**
* Get the JavaScript API URL with key
*/
public function getJavaScriptApiUrl(array $libraries = ['places', 'marker', 'geometry']): string
{
if (empty($this->public_key)) {
return '';
}
$params = [
'key' => $this->public_key,
'libraries' => implode(',', $libraries),
'callback' => 'initGoogleMaps',
];
return $this->apiBase . 'js?' . http_build_query($params);
}
/**
* Enqueue Google Maps script when needed
*/
public function maybeEnqueueMapsScript(): void
{
// Only enqueue if we're on a page that needs maps
if (!$this->shouldEnqueueMaps()) {
return;
}
if (!$this->isSetUp()) {
return;
}
$this->ensureInitialized();
// 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'
]
);
// Add initialization script
wp_add_inline_script('google-maps-api', '
// Set map defaults
window.jvbMapDefaults = {
lat: ' . self::DEFAULT_LAT . ',
lng: ' . self::DEFAULT_LNG . ',
zoom: ' . self::DEFAULT_ZOOM . ',
mapId: "' . esc_js($this->map_id) . '"
};
// 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 || [];
', '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
]);
}
/**
* Check if maps should be enqueued on current page
*/
private function shouldEnqueueMaps(): bool
{
// Check for shortcodes
if (is_singular() && get_post_meta(get_the_ID(), BASE . 'has_map', true) === true) {
return true;
}
if (!empty(Registrar::getFeatured('is_content', 'term')) && get_term_meta(get_queried_object_id(), BASE . 'has_map', true) === true) {
return true;
}
// Check for specific page templates or post types
if (is_singular(['shop', 'artist', 'location'])) {
return true;
}
// Check for admin pages that need maps
if (is_admin()) {
$screen = get_current_screen();
if ($screen && in_array($screen->post_type, ['shop', 'artist', 'location'])) {
return true;
}
}
// Allow filtering
return apply_filters('jvb_should_enqueue_maps', false);
}
/**
* Parse address components from Google response
*/
private function parseAddressComponents(array $components): array
{
$parsed = [
'street_number' => '',
'street' => '',
'city' => '',
'province' => '',
'country' => '',
'postal_code' => ''
];
foreach ($components as $component) {
$types = $component['types'];
if (in_array('street_number', $types)) {
$parsed['street_number'] = $component['long_name'];
}
if (in_array('route', $types)) {
$parsed['street'] = $component['long_name'];
}
if (in_array('locality', $types)) {
$parsed['city'] = $component['long_name'];
}
if (in_array('administrative_area_level_1', $types)) {
$parsed['province'] = $component['short_name'];
}
if (in_array('country', $types)) {
$parsed['country'] = $component['short_name'];
}
if (in_array('postal_code', $types)) {
$parsed['postal_code'] = $component['long_name'];
}
}
// Combine street number and street name
if ($parsed['street_number'] && $parsed['street']) {
$parsed['street'] = $parsed['street_number'] . ' ' . $parsed['street'];
}
return $parsed;
}
/**
* Get Map ID
*/
public function getMapId(): string
{
return $this->map_id;
}
/**
* Get default map settings
*/
public function getDefaultMapSettings(): array
{
return [
'center' => [
'lat' => self::DEFAULT_LAT,
'lng' => self::DEFAULT_LNG
],
'zoom' => $this->credentials['default_zoom'] ?? self::DEFAULT_ZOOM,
'mapId' => $this->map_id,
'styles' => $this->getMapStyles()
];
}
/**
* Get map styles based on theme
*/
private function getMapStyles(): array
{
$theme = $this->credentials['theme'] ?? 'default';
switch ($theme) {
case 'dark':
return [
['elementType' => 'geometry', 'stylers' => [['color' => '#242f3e']]],
['elementType' => 'labels.text.fill', 'stylers' => [['color' => '#746855']]],
['elementType' => 'labels.text.stroke', 'stylers' => [['color' => '#242f3e']]],
['featureType' => 'road', 'elementType' => 'geometry', 'stylers' => [['color' => '#38414e']]],
['featureType' => 'water', 'elementType' => 'geometry', 'stylers' => [['color' => '#17263c']]]
];
case 'minimal':
return [
['featureType' => 'poi', 'stylers' => [['visibility' => 'off']]],
['featureType' => 'transit', 'stylers' => [['visibility' => 'off']]]
];
default:
return [];
}
}
/**
* 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"
];
}
/**
* Render a frontend display map
*/
public function renderDisplayMap(array $location, array $options = []): string
{
if (!$this->isSetUp() || empty($location['lat']) || empty($location['lng'])) {
return '';
}
$map_id = 'map-' . uniqid();
$lat = (float)$location['lat'];
$lng = (float)$location['lng'];
$address = $location['address'] ?? '';
$map_options = array_merge([
'height' => '250px',
'zoom' => 15,
'show_marker' => true,
'show_info_window' => true,
'info_content' => $address,
'interactive' => true
], $options);
ob_start();
?>