mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 10:51:09 -05:00
Add Road Trip Planner template with interactive map and trip management features
- Implemented a new HTML template for the Road Trip Planner. - Integrated Leaflet.js for interactive mapping and routing. - Added functionality for searching and selecting parks to include in a trip. - Enabled drag-and-drop reordering of selected parks. - Included trip optimization and route calculation features. - Created a summary display for trip statistics. - Added functionality to save trips and manage saved trips. - Enhanced UI with responsive design and dark mode support.
This commit is contained in:
850
static/js/map-markers.js
Normal file
850
static/js/map-markers.js
Normal file
@@ -0,0 +1,850 @@
|
||||
/**
|
||||
* 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 = `
|
||||
<style id="map-marker-styles">
|
||||
.location-marker {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.location-marker:hover {
|
||||
transform: scale(1.1);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.location-marker-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
border: 3px solid white;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.location-marker-inner::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 8px solid inherit;
|
||||
}
|
||||
|
||||
.location-marker.size-small .location-marker-inner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.location-marker.size-medium .location-marker-inner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.location-marker.size-large .location-marker-inner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.location-marker-emoji {
|
||||
font-size: 1.2em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.location-marker-icon {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Cluster markers */
|
||||
.cluster-marker {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cluster-marker-inner {
|
||||
background: #3B82F6;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
border: 3px solid white;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.cluster-marker:hover .cluster-marker-inner {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.cluster-marker-small .cluster-marker-inner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cluster-marker-medium .cluster-marker-inner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
.cluster-marker-large .cluster-marker-inner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
background: #DC2626;
|
||||
}
|
||||
|
||||
/* Popup styles */
|
||||
.location-popup {
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 5px 0;
|
||||
color: #1F2937;
|
||||
}
|
||||
|
||||
.popup-subtitle {
|
||||
font-size: 12px;
|
||||
color: #6B7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.popup-detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 4px 0;
|
||||
font-size: 13px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.popup-detail i {
|
||||
width: 16px;
|
||||
margin-right: 6px;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.popup-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.popup-btn {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.popup-btn-primary {
|
||||
background: #3B82F6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.popup-btn-primary:hover {
|
||||
background: #2563EB;
|
||||
}
|
||||
|
||||
.popup-btn-secondary {
|
||||
background: #F3F4F6;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.popup-btn-secondary:hover {
|
||||
background: #E5E7EB;
|
||||
}
|
||||
|
||||
.popup-image {
|
||||
width: 100%;
|
||||
max-height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.popup-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.popup-status.operating {
|
||||
background: #D1FAE5;
|
||||
color: #065F46;
|
||||
}
|
||||
|
||||
.popup-status.closed {
|
||||
background: #FEE2E2;
|
||||
color: #991B1B;
|
||||
}
|
||||
|
||||
.popup-status.construction {
|
||||
background: #EDE9FE;
|
||||
color: #5B21B6;
|
||||
}
|
||||
|
||||
/* Dark mode styles */
|
||||
.dark .popup-title {
|
||||
color: #F9FAFB;
|
||||
}
|
||||
|
||||
.dark .popup-detail {
|
||||
color: #D1D5DB;
|
||||
}
|
||||
|
||||
.dark .popup-btn-secondary {
|
||||
background: #374151;
|
||||
color: #D1D5DB;
|
||||
}
|
||||
|
||||
.dark .popup-btn-secondary:hover {
|
||||
background: #4B5563;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
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 = `<span class="location-marker-emoji">${iconData.emoji}</span>`;
|
||||
break;
|
||||
case 'classic':
|
||||
iconHtml = `<i class="location-marker-icon ${iconData.icon}"></i>`;
|
||||
break;
|
||||
case 'modern':
|
||||
default:
|
||||
iconHtml = location.featured_image ?
|
||||
`<img src="${location.featured_image}" alt="${location.name}" style="width:100%;height:100%;object-fit:cover;border-radius:50%;">` :
|
||||
`<i class="location-marker-icon ${iconData.icon}"></i>`;
|
||||
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: `<div class="location-marker-inner" style="background-color: ${iconData.color}">${iconHtml}</div>`,
|
||||
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 = `
|
||||
<div class="location-popup">
|
||||
${location.featured_image ? `
|
||||
<img src="${location.featured_image}" alt="${location.name}" class="popup-image">
|
||||
` : ''}
|
||||
|
||||
<div class="popup-header">
|
||||
<h3 class="popup-title">${this.escapeHtml(location.name)}</h3>
|
||||
${location.type ? `<p class="popup-subtitle">${this.capitalizeFirst(location.type)}</p>` : ''}
|
||||
${location.status ? `<span class="popup-status ${statusClass}">${this.formatStatus(location.status)}</span>` : ''}
|
||||
</div>
|
||||
|
||||
<div class="popup-content">
|
||||
${this.createPopupDetails(location)}
|
||||
</div>
|
||||
|
||||
<div class="popup-actions">
|
||||
${this.createPopupActions(location)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.popupCache.set(cacheKey, content);
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create popup detail items
|
||||
*/
|
||||
createPopupDetails(location) {
|
||||
const details = [];
|
||||
|
||||
if (location.formatted_location) {
|
||||
details.push(`
|
||||
<div class="popup-detail">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
<span>${this.escapeHtml(location.formatted_location)}</span>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
if (location.operator) {
|
||||
details.push(`
|
||||
<div class="popup-detail">
|
||||
<i class="fas fa-building"></i>
|
||||
<span>${this.escapeHtml(location.operator)}</span>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
if (location.ride_count && location.ride_count > 0) {
|
||||
details.push(`
|
||||
<div class="popup-detail">
|
||||
<i class="fas fa-rocket"></i>
|
||||
<span>${location.ride_count} ride${location.ride_count === 1 ? '' : 's'}</span>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
if (location.opened_date) {
|
||||
details.push(`
|
||||
<div class="popup-detail">
|
||||
<i class="fas fa-calendar"></i>
|
||||
<span>Opened ${this.formatDate(location.opened_date)}</span>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
if (location.manufacturer) {
|
||||
details.push(`
|
||||
<div class="popup-detail">
|
||||
<i class="fas fa-industry"></i>
|
||||
<span>${this.escapeHtml(location.manufacturer)}</span>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
if (location.designer) {
|
||||
details.push(`
|
||||
<div class="popup-detail">
|
||||
<i class="fas fa-pencil-ruler"></i>
|
||||
<span>${this.escapeHtml(location.designer)}</span>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return details.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create popup action buttons
|
||||
*/
|
||||
createPopupActions(location) {
|
||||
const actions = [];
|
||||
|
||||
// View details button
|
||||
actions.push(`
|
||||
<button onclick="mapMarkers.showLocationDetails('${location.type}', ${location.id})"
|
||||
class="popup-btn popup-btn-primary">
|
||||
<i class="fas fa-eye"></i>
|
||||
View Details
|
||||
</button>
|
||||
`);
|
||||
|
||||
// Add to road trip (for parks)
|
||||
if (location.type === 'park' && window.roadTripPlanner) {
|
||||
actions.push(`
|
||||
<button onclick="roadTripPlanner.addPark(${location.id})"
|
||||
class="popup-btn popup-btn-secondary">
|
||||
<i class="fas fa-route"></i>
|
||||
Add to Trip
|
||||
</button>
|
||||
`);
|
||||
}
|
||||
|
||||
// Get directions
|
||||
if (location.latitude && location.longitude) {
|
||||
const mapsUrl = `https://www.google.com/maps/dir/?api=1&destination=${location.latitude},${location.longitude}`;
|
||||
actions.push(`
|
||||
<a href="${mapsUrl}" target="_blank"
|
||||
class="popup-btn popup-btn-secondary">
|
||||
<i class="fas fa-directions"></i>
|
||||
Directions
|
||||
</a>
|
||||
`);
|
||||
}
|
||||
|
||||
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: `<div class="cluster-marker-inner">${count}</div>`,
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user