/** * ThrillWiki Map Markers - Custom Marker Icons and Rich Popup System * * This module handles custom marker icons for different location types, * rich popup content with location details, and performance optimization */ class MapMarkers { constructor(mapInstance, options = {}) { this.mapInstance = mapInstance; this.options = { enableClustering: true, clusterDistance: 50, enableCustomIcons: true, enableRichPopups: true, enableMarkerAnimation: true, popupMaxWidth: 300, iconTheme: 'modern', // 'modern', 'classic', 'emoji' apiEndpoints: { details: '/api/map/location-detail/', media: '/api/media/' }, ...options }; this.markerStyles = this.initializeMarkerStyles(); this.iconCache = new Map(); this.popupCache = new Map(); this.activePopup = null; this.init(); } /** * Initialize the marker system */ init() { this.setupMarkerStyles(); this.setupClusterStyles(); } /** * Initialize marker style definitions */ initializeMarkerStyles() { return { park: { operating: { color: '#10B981', emoji: '🎒', icon: 'fas fa-tree', size: 'large' }, closed_temp: { color: '#F59E0B', emoji: '🚧', icon: 'fas fa-clock', size: 'medium' }, closed_perm: { color: '#EF4444', emoji: '❌', icon: 'fas fa-times-circle', size: 'medium' }, under_construction: { color: '#8B5CF6', emoji: 'πŸ—οΈ', icon: 'fas fa-hard-hat', size: 'medium' }, demolished: { color: '#6B7280', emoji: '🏚️', icon: 'fas fa-ban', size: 'small' } }, ride: { operating: { color: '#3B82F6', emoji: '🎠', icon: 'fas fa-rocket', size: 'medium' }, closed_temp: { color: '#F59E0B', emoji: '⏸️', icon: 'fas fa-pause-circle', size: 'small' }, closed_perm: { color: '#EF4444', emoji: '❌', icon: 'fas fa-times-circle', size: 'small' }, under_construction: { color: '#8B5CF6', emoji: 'πŸ”¨', icon: 'fas fa-tools', size: 'small' }, removed: { color: '#6B7280', emoji: 'πŸ’”', icon: 'fas fa-trash', size: 'small' } }, company: { manufacturer: { color: '#8B5CF6', emoji: '🏭', icon: 'fas fa-industry', size: 'medium' }, operator: { color: '#059669', emoji: '🏒', icon: 'fas fa-building', size: 'medium' }, designer: { color: '#DC2626', emoji: '🎨', icon: 'fas fa-pencil-ruler', size: 'medium' } }, user: { current: { color: '#3B82F6', emoji: 'πŸ“', icon: 'fas fa-crosshairs', size: 'medium' } } }; } /** * Setup marker styles in CSS */ setupMarkerStyles() { if (document.getElementById('map-marker-styles')) return; const styles = ` `; document.head.insertAdjacentHTML('beforeend', styles); } /** * Setup cluster marker styles */ setupClusterStyles() { // Additional cluster-specific styles if needed } /** * Create a location marker */ createLocationMarker(location) { const iconData = this.getMarkerIconData(location); const icon = this.createCustomIcon(iconData, location); const marker = L.marker([location.latitude, location.longitude], { icon: icon, locationData: location, riseOnHover: true }); // Create popup if (this.options.enableRichPopups) { const popupContent = this.createPopupContent(location); marker.bindPopup(popupContent, { maxWidth: this.options.popupMaxWidth, className: 'location-popup-container' }); } // Add click handler marker.on('click', (e) => { this.handleMarkerClick(marker, location); }); // Add hover effects if animation is enabled if (this.options.enableMarkerAnimation) { marker.on('mouseover', () => { const iconElement = marker.getElement(); if (iconElement) { iconElement.style.transform = 'scale(1.1)'; iconElement.style.zIndex = '1000'; } }); marker.on('mouseout', () => { const iconElement = marker.getElement(); if (iconElement) { iconElement.style.transform = 'scale(1)'; iconElement.style.zIndex = ''; } }); } return marker; } /** * Get marker icon data based on location type and status */ getMarkerIconData(location) { const type = location.type || 'generic'; const status = location.status || 'operating'; // Get style data const typeStyles = this.markerStyles[type]; if (!typeStyles) { return this.markerStyles.park.operating; } const statusStyle = typeStyles[status.toLowerCase()]; if (!statusStyle) { // Fallback to first available status for this type const firstStatus = Object.keys(typeStyles)[0]; return typeStyles[firstStatus]; } return statusStyle; } /** * Create custom icon */ createCustomIcon(iconData, location) { const cacheKey = `${location.type}-${location.status}-${this.options.iconTheme}`; if (this.iconCache.has(cacheKey)) { return this.iconCache.get(cacheKey); } let iconHtml; switch (this.options.iconTheme) { case 'emoji': iconHtml = `${iconData.emoji}`; break; case 'classic': iconHtml = ``; break; case 'modern': default: iconHtml = location.featured_image ? `${location.name}` : ``; break; } const sizeClass = iconData.size || 'medium'; const size = sizeClass === 'small' ? 24 : sizeClass === 'large' ? 40 : 32; const icon = L.divIcon({ className: `location-marker size-${sizeClass}`, html: `
${iconHtml}
`, iconSize: [size, size], iconAnchor: [size / 2, size / 2], popupAnchor: [0, -(size / 2) - 8] }); this.iconCache.set(cacheKey, icon); return icon; } /** * Create rich popup content */ createPopupContent(location) { const cacheKey = `popup-${location.type}-${location.id}`; if (this.popupCache.has(cacheKey)) { return this.popupCache.get(cacheKey); } const statusClass = this.getStatusClass(location.status); const content = `
${location.featured_image ? ` ${location.name} ` : ''}
`; this.popupCache.set(cacheKey, content); return content; } /** * Create popup detail items */ createPopupDetails(location) { const details = []; if (location.formatted_location) { details.push(` `); } if (location.operator) { details.push(` `); } if (location.ride_count && location.ride_count > 0) { details.push(` `); } if (location.opened_date) { details.push(` `); } if (location.manufacturer) { details.push(` `); } if (location.designer) { details.push(` `); } return details.join(''); } /** * Create popup action buttons */ createPopupActions(location) { const actions = []; // View details button actions.push(` `); // Add to road trip (for parks) if (location.type === 'park' && window.roadTripPlanner) { actions.push(` `); } // Get directions if (location.latitude && location.longitude) { const mapsUrl = `https://www.google.com/maps/dir/?api=1&destination=${location.latitude},${location.longitude}`; actions.push(` Directions `); } return actions.join(''); } /** * Handle marker click events */ handleMarkerClick(marker, location) { this.activePopup = marker.getPopup(); // Load additional data if needed this.loadLocationDetails(location); // Track click event if (typeof gtag !== 'undefined') { gtag('event', 'marker_click', { event_category: 'map', event_label: `${location.type}:${location.id}`, custom_map: { location_type: location.type, location_name: location.name } }); } } /** * Load additional location details */ async loadLocationDetails(location) { try { const response = await fetch(`${this.options.apiEndpoints.details}${location.type}/${location.id}/`); const data = await response.json(); if (data.status === 'success') { // Update popup with additional details if popup is still open if (this.activePopup && this.activePopup.isOpen()) { const updatedContent = this.createPopupContent(data.data); this.activePopup.setContent(updatedContent); } } } catch (error) { console.error('Failed to load location details:', error); } } /** * Show location details modal/page */ showLocationDetails(type, id) { const url = `/${type}/${id}/`; if (typeof htmx !== 'undefined') { htmx.ajax('GET', url, { target: '#location-modal', swap: 'innerHTML' }).then(() => { const modal = document.getElementById('location-modal'); if (modal) { modal.classList.remove('hidden'); } }); } else { window.location.href = url; } } /** * Get CSS class for status */ getStatusClass(status) { if (!status) return ''; const statusLower = status.toLowerCase(); if (statusLower.includes('operating') || statusLower.includes('open')) { return 'operating'; } else if (statusLower.includes('closed') || statusLower.includes('temp')) { return 'closed'; } else if (statusLower.includes('construction') || statusLower.includes('building')) { return 'construction'; } return ''; } /** * Format status for display */ formatStatus(status) { return status.replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, l => l.toUpperCase()); } /** * Format date for display */ formatDate(dateString) { try { const date = new Date(dateString); return date.getFullYear(); } catch (error) { return dateString; } } /** * Capitalize first letter */ capitalizeFirst(str) { return str.charAt(0).toUpperCase() + str.slice(1); } /** * Escape HTML */ escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, m => map[m]); } /** * Create cluster marker */ createClusterMarker(cluster) { const count = cluster.getChildCount(); let sizeClass = 'small'; if (count > 100) sizeClass = 'large'; else if (count > 10) sizeClass = 'medium'; return L.divIcon({ html: `
${count}
`, className: `cluster-marker cluster-marker-${sizeClass}`, iconSize: L.point(sizeClass === 'small' ? 32 : sizeClass === 'medium' ? 40 : 48, sizeClass === 'small' ? 32 : sizeClass === 'medium' ? 40 : 48) }); } /** * Update marker theme */ setIconTheme(theme) { this.options.iconTheme = theme; this.iconCache.clear(); // Re-render all markers if map instance is available if (this.mapInstance && this.mapInstance.markers) { // This would need to be implemented in the main map class console.log(`Icon theme changed to: ${theme}`); } } /** * Clear popup cache */ clearPopupCache() { this.popupCache.clear(); } /** * Clear icon cache */ clearIconCache() { this.iconCache.clear(); } /** * Get marker statistics */ getMarkerStats() { return { iconCacheSize: this.iconCache.size, popupCacheSize: this.popupCache.size, iconTheme: this.options.iconTheme }; } } // Auto-initialize with map instance if available document.addEventListener('DOMContentLoaded', function() { if (window.thrillwikiMap) { window.mapMarkers = new MapMarkers(window.thrillwikiMap); } }); // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = MapMarkers; } else { window.MapMarkers = MapMarkers; }