/** * Find Training Map - Google Maps Integration * * Handles Google Maps initialization, markers, clustering, * and interaction for the Find Training page. * * @package HVAC_Community_Events * @since 2.2.0 */ (function($) { 'use strict'; // Namespace for the module window.HVACTrainingMap = { // Map instance map: null, // Marker collections trainerMarkers: [], venueMarkers: [], // MarkerClusterer instance markerClusterer: null, // Info window instance (reused) infoWindow: null, // Current data trainers: [], venues: [], // Configuration config: { mapElementId: 'hvac-training-map', defaultCenter: { lat: 39.8283, lng: -98.5795 }, defaultZoom: 4, clusterZoom: 8 }, /** * Initialize the map */ init: function() { const self = this; // Check if Google Maps is loaded if (typeof google === 'undefined' || typeof google.maps === 'undefined') { console.error('Google Maps API not loaded'); this.showMapError('Google Maps failed to load. Please refresh the page.'); return; } // Override config with localized data if (typeof hvacFindTraining !== 'undefined') { if (hvacFindTraining.map_center) { this.config.defaultCenter = hvacFindTraining.map_center; } if (hvacFindTraining.default_zoom) { this.config.defaultZoom = parseInt(hvacFindTraining.default_zoom); } } // Create the map this.createMap(); // Create info window this.infoWindow = new google.maps.InfoWindow(); // Load initial data this.loadMapData(); // Bind events this.bindEvents(); }, /** * Create the Google Map instance */ createMap: function() { const mapElement = document.getElementById(this.config.mapElementId); if (!mapElement) { console.error('Map element not found'); return; } // Remove loading indicator mapElement.innerHTML = ''; // Create map with options this.map = new google.maps.Map(mapElement, { center: this.config.defaultCenter, zoom: this.config.defaultZoom, mapTypeControl: true, mapTypeControlOptions: { position: google.maps.ControlPosition.TOP_RIGHT }, streetViewControl: false, fullscreenControl: true, zoomControl: true, zoomControlOptions: { position: google.maps.ControlPosition.RIGHT_CENTER }, styles: this.getMapStyles() }); // Close info window on map click this.map.addListener('click', () => { this.infoWindow.close(); }); }, /** * Get custom map styles */ getMapStyles: function() { return [ { featureType: 'poi', elementType: 'labels', stylers: [{ visibility: 'off' }] }, { featureType: 'transit', elementType: 'labels', stylers: [{ visibility: 'off' }] } ]; }, /** * Load map data via AJAX */ loadMapData: function(filters) { const self = this; const data = { action: 'hvac_get_training_map_data', nonce: hvacFindTraining.nonce }; // Add filters if provided if (filters) { Object.assign(data, filters); } $.ajax({ url: hvacFindTraining.ajax_url, type: 'POST', data: data, beforeSend: function() { self.showLoading(); }, success: function(response) { if (response.success) { self.trainers = response.data.trainers || []; self.venues = response.data.venues || []; self.updateMarkers(); self.updateCounts(response.data.total_trainers, response.data.total_venues); self.updateTrainerGrid(); } else { self.showMapError(response.data?.message || 'Failed to load data'); } }, error: function() { self.showMapError('Network error. Please try again.'); }, complete: function() { self.hideLoading(); } }); }, /** * Update markers on the map */ updateMarkers: function() { // Clear existing markers this.clearMarkers(); // Check toggle states const showTrainers = $('#hvac-show-trainers').is(':checked'); const showVenues = $('#hvac-show-venues').is(':checked'); // Add trainer markers if (showTrainers && this.trainers.length > 0) { this.trainers.forEach(trainer => { if (trainer.lat && trainer.lng) { this.addTrainerMarker(trainer); } }); } // Add venue markers if (showVenues && this.venues.length > 0) { this.venues.forEach(venue => { if (venue.lat && venue.lng) { this.addVenueMarker(venue); } }); } // Initialize clustering this.initClustering(); // Fit bounds if we have markers this.fitBounds(); }, /** * Add a trainer marker */ addTrainerMarker: function(trainer) { const self = this; // Create marker with custom icon const marker = new google.maps.Marker({ position: { lat: trainer.lat, lng: trainer.lng }, map: this.map, title: trainer.name, icon: this.getTrainerIcon(), optimized: true }); // Store trainer data on marker marker.trainerData = trainer; marker.markerType = 'trainer'; // Add click listener marker.addListener('click', function() { self.showTrainerInfoWindow(this); }); this.trainerMarkers.push(marker); }, /** * Add a venue marker */ addVenueMarker: function(venue) { const self = this; // Create marker with custom icon const marker = new google.maps.Marker({ position: { lat: venue.lat, lng: venue.lng }, map: this.map, title: venue.name, icon: this.getVenueIcon(), optimized: true }); // Store venue data on marker marker.venueData = venue; marker.markerType = 'venue'; // Add click listener marker.addListener('click', function() { self.showVenueInfoWindow(this); }); this.venueMarkers.push(marker); }, /** * Get trainer marker icon */ getTrainerIcon: function() { // Use custom icon if available, otherwise use SVG circle if (hvacFindTraining.marker_icons?.trainer) { return { url: hvacFindTraining.marker_icons.trainer, scaledSize: new google.maps.Size(32, 32) }; } // SVG circle marker (teal) return { path: google.maps.SymbolPath.CIRCLE, fillColor: '#00b3a4', fillOpacity: 1, strokeColor: '#ffffff', strokeWeight: 2, scale: 10 }; }, /** * Get venue marker icon */ getVenueIcon: function() { // Use custom icon if available, otherwise use SVG marker if (hvacFindTraining.marker_icons?.venue) { return { url: hvacFindTraining.marker_icons.venue, scaledSize: new google.maps.Size(32, 32) }; } // SVG marker (orange) return { path: google.maps.SymbolPath.BACKWARD_CLOSED_ARROW, fillColor: '#f5a623', fillOpacity: 1, strokeColor: '#ffffff', strokeWeight: 2, scale: 6 }; }, /** * Initialize marker clustering */ initClustering: function() { // Clear existing clusterer if (this.markerClusterer) { this.markerClusterer.clearMarkers(); } // Combine all markers const allMarkers = [...this.trainerMarkers, ...this.venueMarkers]; if (allMarkers.length === 0) { return; } // Check if MarkerClusterer is available if (typeof markerClusterer !== 'undefined' && markerClusterer.MarkerClusterer) { this.markerClusterer = new markerClusterer.MarkerClusterer({ map: this.map, markers: allMarkers, algorithmOptions: { maxZoom: this.config.clusterZoom } }); } }, /** * Clear all markers from the map */ clearMarkers: function() { // Clear trainer markers this.trainerMarkers.forEach(marker => marker.setMap(null)); this.trainerMarkers = []; // Clear venue markers this.venueMarkers.forEach(marker => marker.setMap(null)); this.venueMarkers = []; // Clear clusterer if (this.markerClusterer) { this.markerClusterer.clearMarkers(); } }, /** * Fit map bounds to show all markers */ fitBounds: function() { const allMarkers = [...this.trainerMarkers, ...this.venueMarkers]; if (allMarkers.length === 0) { // Reset to default view this.map.setCenter(this.config.defaultCenter); this.map.setZoom(this.config.defaultZoom); return; } if (allMarkers.length === 1) { // Single marker - center on it this.map.setCenter(allMarkers[0].getPosition()); this.map.setZoom(10); return; } // Calculate bounds const bounds = new google.maps.LatLngBounds(); allMarkers.forEach(marker => { bounds.extend(marker.getPosition()); }); this.map.fitBounds(bounds, { padding: 50 }); }, /** * Show trainer info window */ showTrainerInfoWindow: function(marker) { const self = this; const trainer = marker.trainerData; // Create DOM elements safely to avoid XSS const container = document.createElement('div'); container.className = 'hvac-info-window'; const title = document.createElement('div'); title.className = 'hvac-info-window-title'; title.textContent = trainer.name; container.appendChild(title); const location = document.createElement('div'); location.className = 'hvac-info-window-location'; location.textContent = (trainer.city || '') + ', ' + (trainer.state || ''); container.appendChild(location); if (trainer.certification) { const certBadge = document.createElement('span'); certBadge.className = 'hvac-info-window-cert'; certBadge.textContent = trainer.certification; container.appendChild(certBadge); } const button = document.createElement('button'); button.className = 'hvac-info-window-btn'; button.textContent = 'View Profile'; button.addEventListener('click', function() { self.openTrainerModal(trainer.profile_id); }); container.appendChild(button); this.infoWindow.setContent(container); this.infoWindow.open(this.map, marker); }, /** * Show venue info window */ showVenueInfoWindow: function(marker) { const self = this; const venue = marker.venueData; const eventsText = venue.upcoming_events > 0 ? venue.upcoming_events + ' upcoming event' + (venue.upcoming_events > 1 ? 's' : '') : 'No upcoming events'; // Create DOM elements safely to avoid XSS const container = document.createElement('div'); container.className = 'hvac-info-window-venue'; const title = document.createElement('div'); title.className = 'hvac-info-window-title'; title.textContent = venue.name; container.appendChild(title); const address = document.createElement('div'); address.className = 'hvac-info-window-address'; address.innerHTML = this.escapeHtml(venue.address || '') + '
' + this.escapeHtml(venue.city || '') + ', ' + this.escapeHtml(venue.state || ''); container.appendChild(address); const eventsCount = document.createElement('div'); eventsCount.className = 'hvac-info-window-events-count'; eventsCount.textContent = eventsText; container.appendChild(eventsCount); const button = document.createElement('button'); button.className = 'hvac-info-window-btn'; button.textContent = 'View Details'; button.addEventListener('click', function() { self.openVenueModal(venue.id); }); container.appendChild(button); this.infoWindow.setContent(container); this.infoWindow.open(this.map, marker); }, /** * Open trainer profile modal */ openTrainerModal: function(profileId) { const self = this; const $modal = $('#hvac-trainer-modal'); const $body = $modal.find('.hvac-modal-body'); const $loading = $modal.find('.hvac-modal-loading'); // Show modal with loading $modal.show(); $loading.show(); $body.hide(); // Close info window this.infoWindow.close(); // Fetch profile data $.ajax({ url: hvacFindTraining.ajax_url, type: 'POST', data: { action: 'hvac_get_trainer_profile_modal', nonce: hvacFindTraining.nonce, profile_id: profileId }, success: function(response) { if (response.success) { $body.html(response.data.html); $loading.hide(); $body.show(); self.bindContactForm(); } else { $body.html('

Failed to load profile.

'); $loading.hide(); $body.show(); } }, error: function() { $body.html('

Network error. Please try again.

'); $loading.hide(); $body.show(); } }); }, /** * Open venue details modal */ openVenueModal: function(venueId) { const self = this; const $modal = $('#hvac-venue-modal'); // Close info window this.infoWindow.close(); // Fetch venue data $.ajax({ url: hvacFindTraining.ajax_url, type: 'POST', data: { action: 'hvac_get_venue_info', nonce: hvacFindTraining.nonce, venue_id: venueId }, success: function(response) { if (response.success) { const venue = response.data.venue; self.populateVenueModal(venue); $modal.show(); } } }); }, /** * Populate venue modal with data */ populateVenueModal: function(venue) { const $modal = $('#hvac-venue-modal'); // Title $modal.find('#venue-modal-title').text(venue.name); // Address const addressParts = [venue.address, venue.city, venue.state].filter(Boolean); $modal.find('.hvac-venue-address').text(addressParts.join(', ')); // Events list const $eventsList = $modal.find('.hvac-venue-events-list'); $eventsList.empty(); if (venue.events && venue.events.length > 0) { venue.events.forEach(event => { $eventsList.append(`
  • ${this.escapeHtml(event.title)} ${this.escapeHtml(event.date)}
  • `); }); } else { $eventsList.html('
  • No upcoming events at this venue.
  • '); } // Directions link const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${venue.lat},${venue.lng}`; $modal.find('.hvac-venue-directions').attr('href', directionsUrl); }, /** * Update trainer directory grid */ updateTrainerGrid: function() { const $grid = $('#hvac-trainer-grid'); $grid.empty(); if (this.trainers.length === 0) { $grid.html('

    No trainers found matching your criteria.

    '); return; } // Display trainers (show first 12, load more on request) const displayCount = Math.min(this.trainers.length, 12); for (let i = 0; i < displayCount; i++) { const trainer = this.trainers[i]; $grid.append(this.createTrainerCard(trainer)); } // Show load more if there are more trainers if (this.trainers.length > 12) { $('.hvac-load-more-wrapper').show(); } else { $('.hvac-load-more-wrapper').hide(); } }, /** * Create trainer card HTML */ createTrainerCard: function(trainer) { const imageHtml = trainer.image ? `${this.escapeHtml(trainer.name)}` : '
    '; const certHtml = trainer.certifications && trainer.certifications.length > 0 ? trainer.certifications.map(cert => `${this.escapeHtml(cert)}`).join('') : ''; return `
    ${imageHtml}
    ${this.escapeHtml(trainer.name)}
    ${this.escapeHtml(trainer.city)}, ${this.escapeHtml(trainer.state)}
    ${certHtml}
    `; }, /** * Update counts display */ updateCounts: function(trainers, venues) { $('#hvac-trainer-count').text(trainers || 0); $('#hvac-venue-count').text(venues || 0); }, /** * Bind event handlers */ bindEvents: function() { const self = this; // Trainer card click $(document).on('click', '.hvac-trainer-card', function() { const profileId = $(this).data('profile-id'); if (profileId) { self.openTrainerModal(profileId); } }); // Modal close $(document).on('click', '.hvac-modal-close, .hvac-modal-overlay', function() { $('.hvac-training-modal').hide(); }); // ESC key to close modal $(document).on('keydown', function(e) { if (e.key === 'Escape') { $('.hvac-training-modal').hide(); } }); // Marker toggles $('#hvac-show-trainers, #hvac-show-venues').on('change', function() { self.updateMarkers(); }); // Load more button $('#hvac-load-more').on('click', function() { self.loadMoreTrainers(); }); }, /** * Bind contact form in modal */ bindContactForm: function() { const self = this; $('.hvac-training-contact-form').off('submit').on('submit', function(e) { e.preventDefault(); self.submitContactForm($(this)); }); }, /** * Submit contact form */ submitContactForm: function($form) { const trainerId = $form.data('trainer-id'); const profileId = $form.data('profile-id'); const $successMsg = $form.siblings('.hvac-form-success'); const $errorMsg = $form.siblings('.hvac-form-error'); const $submit = $form.find('button[type="submit"]'); // Collect form data const formData = { action: 'hvac_submit_contact_form', nonce: hvacFindTraining.nonce, trainer_id: trainerId, trainer_profile_id: profileId }; $form.serializeArray().forEach(field => { formData[field.name] = field.value; }); // Submit $submit.prop('disabled', true).text('Sending...'); $successMsg.hide(); $errorMsg.hide(); $.ajax({ url: hvacFindTraining.ajax_url, type: 'POST', data: formData, success: function(response) { if (response.success) { $form.hide(); $successMsg.show(); } else { $errorMsg.show(); } }, error: function() { $errorMsg.show(); }, complete: function() { $submit.prop('disabled', false).text('Send Message'); } }); }, /** * Load more trainers */ loadMoreTrainers: function() { const $grid = $('#hvac-trainer-grid'); const currentCount = $grid.find('.hvac-trainer-card').length; const loadMore = 12; for (let i = currentCount; i < currentCount + loadMore && i < this.trainers.length; i++) { $grid.append(this.createTrainerCard(this.trainers[i])); } if ($grid.find('.hvac-trainer-card').length >= this.trainers.length) { $('.hvac-load-more-wrapper').hide(); } }, /** * Get user's location */ getUserLocation: function(callback) { if (!navigator.geolocation) { callback(null, hvacFindTraining.messages.geolocation_unsupported); return; } navigator.geolocation.getCurrentPosition( function(position) { callback({ lat: position.coords.latitude, lng: position.coords.longitude }); }, function(error) { callback(null, hvacFindTraining.messages.geolocation_error); } ); }, /** * Center map on location */ centerOnLocation: function(lat, lng, zoom) { this.map.setCenter({ lat: lat, lng: lng }); this.map.setZoom(zoom || 10); }, /** * Show loading state */ showLoading: function() { $('#hvac-trainer-grid').html('
    Loading trainers...
    '); }, /** * Hide loading state */ hideLoading: function() { $('#hvac-trainer-grid .hvac-grid-loading').remove(); }, /** * Show map error */ showMapError: function(message) { const $map = $('#' + this.config.mapElementId); $map.html(`

    ${this.escapeHtml(message)}

    `); }, /** * Escape HTML for safe output */ escapeHtml: function(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } }; // Initialize when document is ready $(document).ready(function() { // Wait a moment for Google Maps to fully load if ($('#hvac-training-map').length) { setTimeout(function() { HVACTrainingMap.init(); }, 100); } }); })(jQuery);