cache_group); if ($cached !== false && empty($filters)) { return $cached; } // Get approved user IDs $approved_user_ids = $this->get_approved_user_ids(); if (empty($approved_user_ids)) { return []; } // Build query args $query_args = [ 'post_type' => 'trainer_profile', 'posts_per_page' => -1, 'post_status' => 'publish', 'meta_query' => [ 'relation' => 'AND', [ 'key' => 'is_public_profile', 'value' => '1', 'compare' => '=' ], [ 'key' => 'user_id', 'value' => $approved_user_ids, 'compare' => 'IN' ], [ 'key' => 'latitude', 'compare' => 'EXISTS' ], [ 'key' => 'longitude', 'compare' => 'EXISTS' ], [ 'key' => 'latitude', 'value' => '', 'compare' => '!=' ], [ 'key' => 'longitude', 'value' => '', 'compare' => '!=' ] ] ]; // Add state filter if (!empty($filters['state'])) { $query_args['meta_query'][] = [ 'key' => 'trainer_state', 'value' => sanitize_text_field($filters['state']), 'compare' => '=' ]; } // Add search filter if (!empty($filters['search'])) { $search = sanitize_text_field($filters['search']); $query_args['meta_query'][] = [ 'relation' => 'OR', [ 'key' => 'trainer_display_name', 'value' => $search, 'compare' => 'LIKE' ], [ 'key' => 'trainer_city', 'value' => $search, 'compare' => 'LIKE' ], [ 'key' => 'company_name', 'value' => $search, 'compare' => 'LIKE' ] ]; } $query = new WP_Query($query_args); $markers = []; if ($query->have_posts()) { while ($query->have_posts()) { $query->the_post(); $profile_id = get_the_ID(); $marker = $this->format_trainer_marker($profile_id); // Apply certification filter if (!empty($filters['certification'])) { $cert_match = false; foreach ($marker['certifications'] as $cert) { if (stripos($cert, $filters['certification']) !== false) { $cert_match = true; break; } } if (!$cert_match) { continue; } } // Apply proximity filter if (!empty($filters['lat']) && !empty($filters['lng']) && !empty($filters['radius'])) { $distance = $this->calculate_distance( $filters['lat'], $filters['lng'], $marker['lat'], $marker['lng'] ); if ($distance > $filters['radius']) { continue; } $marker['distance'] = round($distance, 1); } $markers[] = $marker; } } wp_reset_postdata(); // Sort by distance if proximity search if (!empty($filters['lat']) && !empty($filters['lng'])) { usort($markers, function($a, $b) { return ($a['distance'] ?? 0) <=> ($b['distance'] ?? 0); }); } // Cache if no filters if (empty($filters)) { wp_cache_set($cache_key, $markers, $this->cache_group, $this->cache_expiration); } return $markers; } /** * Get venue markers for map * * @param array $filters Optional filters * @return array Venue markers data */ public function get_venue_markers(array $filters = []): array { // Check if TEC is active if (!function_exists('tribe_get_venue')) { return []; } // Generate cache key $cache_key = 'venues_' . md5(serialize($filters)); $cached = wp_cache_get($cache_key, $this->cache_group); if ($cached !== false && empty($filters)) { return $cached; } // Build query args $query_args = [ 'post_type' => 'tribe_venue', 'posts_per_page' => -1, 'post_status' => 'publish', 'meta_query' => [ 'relation' => 'AND', [ 'relation' => 'OR', // Check for our custom venue coordinates [ 'key' => 'venue_latitude', 'compare' => 'EXISTS' ], // Also check TEC's built-in coordinates [ 'key' => '_VenueLat', 'compare' => 'EXISTS' ] ] ] ]; // Add state filter if (!empty($filters['state'])) { $query_args['meta_query'][] = [ 'relation' => 'OR', [ 'key' => '_VenueStateProvince', 'value' => sanitize_text_field($filters['state']), 'compare' => '=' ], [ 'key' => '_VenueState', 'value' => sanitize_text_field($filters['state']), 'compare' => '=' ] ]; } // Add search filter if (!empty($filters['search'])) { $query_args['s'] = sanitize_text_field($filters['search']); } $query = new WP_Query($query_args); $markers = []; if ($query->have_posts()) { while ($query->have_posts()) { $query->the_post(); $venue_id = get_the_ID(); $marker = $this->format_venue_marker($venue_id); // Skip venues without valid coordinates if (empty($marker['lat']) || empty($marker['lng'])) { continue; } // Apply proximity filter if (!empty($filters['lat']) && !empty($filters['lng']) && !empty($filters['radius'])) { $distance = $this->calculate_distance( $filters['lat'], $filters['lng'], $marker['lat'], $marker['lng'] ); if ($distance > $filters['radius']) { continue; } $marker['distance'] = round($distance, 1); } $markers[] = $marker; } } wp_reset_postdata(); // Sort by distance if proximity search if (!empty($filters['lat']) && !empty($filters['lng'])) { usort($markers, function($a, $b) { return ($a['distance'] ?? 0) <=> ($b['distance'] ?? 0); }); } // Cache if no filters if (empty($filters)) { wp_cache_set($cache_key, $markers, $this->cache_group, $this->cache_expiration); } return $markers; } /** * Format trainer data for map marker * * @param int $profile_id Trainer profile post ID * @return array Formatted marker data */ private function format_trainer_marker(int $profile_id): array { $user_id = get_post_meta($profile_id, 'user_id', true); $lat = get_post_meta($profile_id, 'latitude', true); $lng = get_post_meta($profile_id, 'longitude', true); // Get certifications $certifications = $this->get_trainer_certifications($profile_id, $user_id); // Get event count (cached) $event_count = get_post_meta($profile_id, 'cached_event_count', true); if (empty($event_count)) { $event_count = 0; } return [ 'id' => $profile_id, 'type' => 'trainer', 'lat' => floatval($lat), 'lng' => floatval($lng), 'name' => get_post_meta($profile_id, 'trainer_display_name', true), 'city' => get_post_meta($profile_id, 'trainer_city', true), 'state' => get_post_meta($profile_id, 'trainer_state', true), 'certifications' => $certifications, 'certification' => !empty($certifications) ? $certifications[0] : '', 'image' => get_post_meta($profile_id, 'profile_image_url', true), 'profile_id' => $profile_id, 'user_id' => intval($user_id), 'event_count' => intval($event_count) ]; } /** * Format venue data for map marker * * @param int $venue_id Venue post ID * @return array Formatted marker data */ private function format_venue_marker(int $venue_id): array { // Try our custom coordinates first, then TEC's $lat = get_post_meta($venue_id, 'venue_latitude', true); $lng = get_post_meta($venue_id, 'venue_longitude', true); if (empty($lat) || empty($lng)) { $lat = get_post_meta($venue_id, '_VenueLat', true); $lng = get_post_meta($venue_id, '_VenueLng', true); } // Get venue details from TEC $city = get_post_meta($venue_id, '_VenueCity', true); $state = get_post_meta($venue_id, '_VenueStateProvince', true) ?: get_post_meta($venue_id, '_VenueState', true); $address = get_post_meta($venue_id, '_VenueAddress', true); // Count upcoming events at this venue $upcoming_events_count = $this->count_venue_upcoming_events($venue_id); return [ 'id' => $venue_id, 'type' => 'venue', 'lat' => floatval($lat), 'lng' => floatval($lng), 'name' => get_the_title($venue_id), 'address' => $address, 'city' => $city, 'state' => $state, 'upcoming_events' => $upcoming_events_count ]; } /** * Get trainer certifications * * @param int $profile_id Profile post ID * @param int $user_id User ID * @return array List of certification names */ private function get_trainer_certifications(int $profile_id, int $user_id): array { $certifications = []; // Try new certification system if (class_exists('HVAC_Trainer_Certification_Manager')) { $cert_manager = HVAC_Trainer_Certification_Manager::instance(); $trainer_certs = $cert_manager->get_trainer_certifications($user_id); foreach ($trainer_certs as $cert) { $cert_type = get_post_meta($cert->ID, 'certification_type', true); $status = get_post_meta($cert->ID, 'status', true) ?: 'active'; $expiration = get_post_meta($cert->ID, 'expiration_date', true); // Only include active, non-expired certifications $is_expired = $expiration && strtotime($expiration) < time(); if ($status === 'active' && !$is_expired && !empty($cert_type)) { $certifications[] = $cert_type; } } } // Fallback to legacy certification if (empty($certifications)) { $legacy = get_post_meta($profile_id, 'certification_type', true); if (!empty($legacy)) { $certifications[] = $legacy; } } return array_unique($certifications); } /** * Count upcoming events at a venue * * @param int $venue_id Venue post ID * @return int Number of upcoming events */ private function count_venue_upcoming_events(int $venue_id): int { if (!function_exists('tribe_get_events')) { return 0; } $events = tribe_get_events([ 'eventDisplay' => 'upcoming', 'posts_per_page' => -1, 'venue' => $venue_id, 'fields' => 'ids' ]); return is_array($events) ? count($events) : 0; } /** * Get full trainer profile for modal * * @param int $profile_id Profile post ID * @return array|null Trainer data or null if not found */ public function get_trainer_full_profile(int $profile_id): ?array { $profile = get_post($profile_id); if (!$profile || $profile->post_type !== 'trainer_profile') { return null; } $user_id = get_post_meta($profile_id, 'user_id', true); // Get basic marker data $data = $this->format_trainer_marker($profile_id); // Add additional details for modal $data['company'] = get_post_meta($profile_id, 'company_name', true); $data['bio'] = get_post_meta($profile_id, 'trainer_bio', true); $data['training_formats'] = $this->get_meta_array($profile_id, 'training_formats'); $data['training_locations'] = $this->get_meta_array($profile_id, 'training_locations'); // Get upcoming events $data['upcoming_events'] = $this->get_trainer_upcoming_events($user_id); return $data; } /** * Get full venue info * * @param int $venue_id Venue post ID * @return array|null Venue data or null if not found */ public function get_venue_full_info(int $venue_id): ?array { $venue = get_post($venue_id); if (!$venue || $venue->post_type !== 'tribe_venue') { return null; } $data = $this->format_venue_marker($venue_id); // Add additional details $data['zip'] = get_post_meta($venue_id, '_VenueZip', true); $data['country'] = get_post_meta($venue_id, '_VenueCountry', true); $data['phone'] = get_post_meta($venue_id, '_VenuePhone', true); $data['website'] = get_post_meta($venue_id, '_VenueURL', true); // Get upcoming events list if (function_exists('tribe_get_events')) { $events = tribe_get_events([ 'eventDisplay' => 'upcoming', 'posts_per_page' => 5, 'venue' => $venue_id ]); $data['events'] = []; foreach ($events as $event) { $data['events'][] = [ 'id' => $event->ID, 'title' => $event->post_title, 'date' => tribe_get_start_date($event->ID, false, 'M j, Y'), 'url' => get_permalink($event->ID) ]; } } return $data; } /** * Get trainer's upcoming events * * @param int $user_id User ID * @param int $limit Maximum events to return * @return array Upcoming events */ private function get_trainer_upcoming_events(int $user_id, int $limit = 5): array { if (!function_exists('tribe_get_events')) { return []; } $events = tribe_get_events([ 'author' => $user_id, 'eventDisplay' => 'upcoming', 'posts_per_page' => $limit ]); $formatted = []; foreach ($events as $event) { $formatted[] = [ 'id' => $event->ID, 'title' => $event->post_title, 'date' => tribe_get_start_date($event->ID, false, 'M j, Y'), 'url' => get_permalink($event->ID) ]; } return $formatted; } /** * Get approved user IDs for filtering trainer profiles * * @return array User IDs */ private function get_approved_user_ids(): array { $user_query = new WP_User_Query([ 'meta_query' => [ [ 'key' => 'account_status', 'value' => ['approved', 'active', 'inactive'], 'compare' => 'IN' ] ], 'fields' => 'ID' ]); return $user_query->get_results(); } /** * Get meta value as array (handles comma-separated or serialized) * * @param int $post_id Post ID * @param string $meta_key Meta key * @return array Values */ private function get_meta_array(int $post_id, string $meta_key): array { $value = get_post_meta($post_id, $meta_key, true); if (empty($value)) { return []; } if (is_array($value)) { return $value; } // Handle comma-separated if (strpos($value, ',') !== false) { return array_map('trim', explode(',', $value)); } return [$value]; } /** * Calculate distance between two coordinates using Haversine formula * * @param float $lat1 First latitude * @param float $lng1 First longitude * @param float $lat2 Second latitude * @param float $lng2 Second longitude * @return float Distance in kilometers */ private function calculate_distance(float $lat1, float $lng1, float $lat2, float $lng2): float { $earth_radius = 6371; // km $lat1_rad = deg2rad($lat1); $lat2_rad = deg2rad($lat2); $delta_lat = deg2rad($lat2 - $lat1); $delta_lng = deg2rad($lng2 - $lng1); $a = sin($delta_lat / 2) * sin($delta_lat / 2) + cos($lat1_rad) * cos($lat2_rad) * sin($delta_lng / 2) * sin($delta_lng / 2); $c = 2 * atan2(sqrt($a), sqrt(1 - $a)); return $earth_radius * $c; } /** * Get unique state options from trainers * * @return array State options */ public function get_state_options(): array { // Check cache first $cache_key = 'filter_state_options'; $cached = wp_cache_get($cache_key, $this->cache_group); if ($cached !== false) { return $cached; } global $wpdb; $states = $wpdb->get_col(" SELECT DISTINCT pm.meta_value FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE pm.meta_key = 'trainer_state' AND p.post_type = 'trainer_profile' AND p.post_status = 'publish' AND pm.meta_value != '' ORDER BY pm.meta_value ASC "); $states = array_filter($states); // Cache for 1 hour wp_cache_set($cache_key, $states, $this->cache_group, $this->cache_expiration); return $states; } /** * Get certification type options * * @return array Certification options */ public function get_certification_options(): array { // Check cache first $cache_key = 'filter_certification_options'; $cached = wp_cache_get($cache_key, $this->cache_group); if ($cached !== false) { return $cached; } global $wpdb; $certs = $wpdb->get_col(" SELECT DISTINCT pm.meta_value FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE pm.meta_key = 'certification_type' AND p.post_type = 'trainer_profile' AND p.post_status = 'publish' AND pm.meta_value != '' ORDER BY pm.meta_value ASC "); $certs = array_filter($certs); // Cache for 1 hour wp_cache_set($cache_key, $certs, $this->cache_group, $this->cache_expiration); return $certs; } /** * Get training format options * * @return array Training format options */ public function get_training_format_options(): array { // Check cache first $cache_key = 'filter_format_options'; $cached = wp_cache_get($cache_key, $this->cache_group); if ($cached !== false) { return $cached; } global $wpdb; $formats_raw = $wpdb->get_col(" SELECT DISTINCT pm.meta_value FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE pm.meta_key = 'training_formats' AND p.post_type = 'trainer_profile' AND p.post_status = 'publish' AND pm.meta_value != '' "); // Process comma-separated values $formats = []; foreach ($formats_raw as $format_string) { if (empty($format_string)) continue; $individual = array_map('trim', explode(',', $format_string)); $formats = array_merge($formats, $individual); } $formats = array_unique(array_filter($formats)); sort($formats); // Cache for 1 hour wp_cache_set($cache_key, $formats, $this->cache_group, $this->cache_expiration); return $formats; } /** * Clear trainer cache */ public function clear_trainer_cache(): void { if (function_exists('wp_cache_delete_group')) { wp_cache_delete_group($this->cache_group); } else { wp_cache_flush(); } } /** * Clear venue cache */ public function clear_venue_cache(): void { if (function_exists('wp_cache_delete_group')) { wp_cache_delete_group($this->cache_group); } else { wp_cache_flush(); } } }