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']; } }