upskill-event-manager/includes/find-training/class-hvac-training-map-data.php
ben fcd55fd164 feat(find-training): Differentiate measureQuick Certified Champions from Trainers
Champions are identified by "Certified measureQuick Champion" certification.
Unlike Trainers, Champions do not offer public training, so they display
differently:

- White marker outline (vs green for Trainers)
- Show only state, not city, in sidebar and info windows
- No "View Profile" button or modal popup on click
- Sorted to end of trainer list (after all Trainers)
- Non-clickable card styling

Code review fixes (Gemini 3):
- Fixed location formatting to handle empty city gracefully
- Added secondary sort by name for stable ordering

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 02:01:15 -04:00

1102 lines
35 KiB
PHP

<?php
/**
* Training Map Data Provider
*
* Provides marker data for trainers and venues on the Find Training map.
*
* @package HVAC_Community_Events
* @since 2.2.0
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class HVAC_Training_Map_Data
*
* Data provider for the Find Training Google Maps integration.
* Queries trainers and venues with coordinates for map display.
*/
class HVAC_Training_Map_Data {
/**
* Singleton instance
*
* @var HVAC_Training_Map_Data|null
*/
private static ?self $instance = null;
/**
* Cache group for queries
*
* @var string
*/
private string $cache_group = 'hvac_training_map';
/**
* Cache expiration (1 hour)
*
* @var int
*/
private int $cache_expiration = 3600;
/**
* Get singleton instance
*
* @return HVAC_Training_Map_Data
*/
public static function get_instance(): self {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
// Clear cache when profiles, venues, or events are updated
add_action('save_post_trainer_profile', [$this, 'clear_trainer_cache']);
add_action('save_post_tribe_venue', [$this, 'clear_venue_cache']);
add_action('save_post_tribe_events', [$this, 'clear_event_cache']);
}
/**
* Get trainer markers for map
*
* @param array $filters Optional filters
* @return array Trainer markers data
*/
public function get_trainer_markers(array $filters = []): array {
// Generate cache key based on filters
$cache_key = 'trainers_' . md5(serialize($filters));
$cached = wp_cache_get($cache_key, $this->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
*
* Filters to show only measureQuick Approved Training Labs.
*
* @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_approved_labs_' . md5(serialize($filters));
$cached = wp_cache_get($cache_key, $this->cache_group);
if ($cached !== false && empty($filters)) {
return $cached;
}
// Build query args - filter for approved training labs only
$query_args = [
'post_type' => 'tribe_venue',
'posts_per_page' => -1,
'post_status' => 'publish',
// Only show venues tagged as measureQuick Approved Training Labs
'tax_query' => [
[
'taxonomy' => 'venue_type',
'field' => 'slug',
'terms' => 'mq-approved-lab',
],
],
'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);
// Determine if this is a champion (not a trainer offering public training)
$is_champion = in_array('Certified measureQuick Champion', $certifications, true);
// 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',
'is_champion' => $is_champion,
'lat' => floatval($lat),
'lng' => floatval($lng),
'name' => get_post_meta($profile_id, 'trainer_display_name', true),
'city' => $is_champion ? '' : 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' => html_entity_decode(get_the_title($venue_id), ENT_QUOTES, 'UTF-8'),
'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);
$data['description'] = $venue->post_content;
$data['capacity'] = get_post_meta($venue_id, '_VenueCapacity', true);
// Get equipment and amenities taxonomies
$equipment = wp_get_post_terms($venue_id, 'venue_equipment', ['fields' => 'names']);
$data['equipment'] = is_wp_error($equipment) ? [] : $equipment;
$amenities = wp_get_post_terms($venue_id, 'venue_amenities', ['fields' => 'names']);
$data['amenities'] = is_wp_error($amenities) ? [] : $amenities;
// Get POC (Point of Contact) information
$data['poc_user_id'] = get_post_meta($venue_id, '_venue_poc_user_id', true);
$data['poc_name'] = get_post_meta($venue_id, '_venue_poc_name', true);
$data['poc_email'] = get_post_meta($venue_id, '_venue_poc_email', true);
// If no POC meta, use post author as fallback
if (empty($data['poc_user_id'])) {
$data['poc_user_id'] = $venue->post_author;
$author = get_userdata($venue->post_author);
if ($author) {
$data['poc_name'] = $author->display_name;
$data['poc_email'] = $author->user_email;
}
}
// 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' => html_entity_decode($event->post_title, ENT_QUOTES, 'UTF-8'),
'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' => html_entity_decode($event->post_title, ENT_QUOTES, 'UTF-8'),
'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;
}
/**
* Get event markers for map
*
* @param array $filters Optional filters (state, search, lat/lng/radius, include_past)
* @return array Event markers data
*/
public function get_event_markers(array $filters = []): array {
// Check if TEC is active
if (!function_exists('tribe_get_events')) {
return [];
}
// Generate cache key
$cache_key = 'events_' . md5(serialize($filters));
$cached = wp_cache_get($cache_key, $this->cache_group);
if ($cached !== false && empty($filters)) {
return $cached;
}
// Determine if we need to filter by venue first
$venue_ids = null;
if (!empty($filters['state']) || !empty($filters['search']) ||
(!empty($filters['lat']) && !empty($filters['lng']) && !empty($filters['radius']))) {
$venue_ids = $this->get_venue_ids_by_filters($filters);
// If venue filters are active but no matching venues found, return empty
if (!empty($filters['state']) && empty($venue_ids)) {
return [];
}
}
// Build event query args
$include_past = !empty($filters['include_past']);
$event_args = [
'posts_per_page' => 100,
'post_status' => 'publish',
'orderby' => 'event_date',
'order' => 'ASC',
];
if ($include_past) {
// Custom date range: past 6 months to future 1 year
$event_args['eventDisplay'] = 'custom';
$event_args['start_date'] = date('Y-m-d', strtotime('-6 months'));
$event_args['end_date'] = date('Y-m-d', strtotime('+1 year'));
} else {
// Default: upcoming and ongoing events only
$event_args['eventDisplay'] = 'list';
}
// Apply venue filter if we have filtered venue IDs
if ($venue_ids !== null && !empty($venue_ids)) {
$event_args['venue'] = $venue_ids;
}
$events = tribe_get_events($event_args);
$markers = [];
// Batch load meta for performance
if (!empty($events)) {
$event_ids = wp_list_pluck($events, 'ID');
update_postmeta_cache($event_ids);
}
foreach ($events as $event) {
$marker = $this->format_event_marker($event);
// Skip events without valid coordinates
if (empty($marker) || empty($marker['lat']) || empty($marker['lng'])) {
continue;
}
// Apply search filter to event title and venue
if (!empty($filters['search'])) {
$search = strtolower($filters['search']);
$title_match = stripos($marker['title'], $search) !== false;
$venue_match = stripos($marker['venue_name'] ?? '', $search) !== false;
$city_match = stripos($marker['venue_city'] ?? '', $search) !== false;
if (!$title_match && !$venue_match && !$city_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;
}
// Sort by distance if proximity search, otherwise by date
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 event data for map marker
*
* @param WP_Post $event Event post object
* @return array|null Formatted marker data or null if invalid
*/
private function format_event_marker(WP_Post $event): ?array {
// Get venue ID
$venue_id = get_post_meta($event->ID, '_EventVenueID', true);
// Skip events without venue
if (empty($venue_id)) {
return null;
}
// Check if event should show on map
$show_map = get_post_meta($event->ID, '_EventShowMap', true);
if ($show_map === 'false' || $show_map === '0') {
// Virtual-only event with no map display
return null;
}
// Get venue coordinates
$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);
}
// Skip if no coordinates
if (empty($lat) || empty($lng)) {
return null;
}
// Get venue details
$venue_name = get_the_title($venue_id);
$venue_city = get_post_meta($venue_id, '_VenueCity', true);
$venue_state = get_post_meta($venue_id, '_VenueStateProvince', true)
?: get_post_meta($venue_id, '_VenueState', true);
// Get event dates
$start_date = tribe_get_start_date($event->ID, false, 'M j, Y');
$end_date = tribe_get_end_date($event->ID, false, 'M j, Y');
$start_time = tribe_get_start_date($event->ID, false, 'g:i A');
$is_all_day = tribe_event_is_all_day($event->ID);
// Determine if past event
$event_end = tribe_get_end_date($event->ID, false, 'U');
$is_past = $event_end < time();
// Get cost
$cost = tribe_get_formatted_cost($event->ID);
if (empty($cost)) {
$cost = 'Free';
}
// Get excerpt (sanitized)
$excerpt = wp_strip_all_tags($event->post_content);
$excerpt = wp_trim_words($excerpt, 20, '...');
return [
'id' => $event->ID,
'type' => 'event',
'lat' => floatval($lat),
'lng' => floatval($lng),
'title' => esc_html(html_entity_decode(get_the_title($event->ID), ENT_QUOTES, 'UTF-8')),
'excerpt' => $excerpt,
'start_date' => $start_date,
'end_date' => $end_date,
'start_time' => $is_all_day ? '' : $start_time,
'is_all_day' => $is_all_day,
'venue_id' => intval($venue_id),
'venue_name' => esc_html(html_entity_decode($venue_name, ENT_QUOTES, 'UTF-8')),
'venue_city' => $venue_city,
'venue_state' => $venue_state,
'cost' => $cost,
'url' => esc_url(get_permalink($event->ID)),
'is_past' => $is_past
];
}
/**
* Get venue IDs matching filters (for event filtering)
*
* @param array $filters Filters (state, search, lat/lng/radius)
* @return array Matching venue IDs
*/
private function get_venue_ids_by_filters(array $filters): array {
if (!function_exists('tribe_get_venue')) {
return [];
}
$query_args = [
'post_type' => 'tribe_venue',
'posts_per_page' => -1,
'post_status' => 'publish',
'fields' => 'ids',
'meta_query' => [
'relation' => 'AND',
]
];
// 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 (venue name or city)
if (!empty($filters['search'])) {
$query_args['meta_query'][] = [
'relation' => 'OR',
[
'key' => '_VenueCity',
'value' => sanitize_text_field($filters['search']),
'compare' => 'LIKE'
]
];
// Also search by venue name
$query_args['s'] = sanitize_text_field($filters['search']);
}
$query = new WP_Query($query_args);
$venue_ids = $query->posts;
// Apply proximity filter if needed
if (!empty($filters['lat']) && !empty($filters['lng']) && !empty($filters['radius']) && !empty($venue_ids)) {
$filtered_ids = [];
foreach ($venue_ids as $venue_id) {
$lat = get_post_meta($venue_id, 'venue_latitude', true) ?: get_post_meta($venue_id, '_VenueLat', true);
$lng = get_post_meta($venue_id, 'venue_longitude', true) ?: get_post_meta($venue_id, '_VenueLng', true);
if (!empty($lat) && !empty($lng)) {
$distance = $this->calculate_distance(
$filters['lat'],
$filters['lng'],
floatval($lat),
floatval($lng)
);
if ($distance <= $filters['radius']) {
$filtered_ids[] = $venue_id;
}
}
}
$venue_ids = $filtered_ids;
}
return $venue_ids;
}
/**
* Clear event cache
*/
public function clear_event_cache(): void {
if (function_exists('wp_cache_delete_group')) {
wp_cache_delete_group($this->cache_group);
} else {
wp_cache_flush();
}
}
/**
* 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();
}
}
}