mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 05:31:09 -05:00
- 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.
774 lines
26 KiB
JavaScript
774 lines
26 KiB
JavaScript
/**
|
|
* ThrillWiki Road Trip Planner - Multi-park Route Planning
|
|
*
|
|
* This module provides road trip planning functionality with multi-park selection,
|
|
* route visualization, distance calculations, and export capabilities
|
|
*/
|
|
|
|
class RoadTripPlanner {
|
|
constructor(containerId, options = {}) {
|
|
this.containerId = containerId;
|
|
this.options = {
|
|
mapInstance: null,
|
|
maxParks: 20,
|
|
enableOptimization: true,
|
|
enableExport: true,
|
|
apiEndpoints: {
|
|
parks: '/api/parks/',
|
|
route: '/api/roadtrip/route/',
|
|
optimize: '/api/roadtrip/optimize/',
|
|
export: '/api/roadtrip/export/'
|
|
},
|
|
routeOptions: {
|
|
color: '#3B82F6',
|
|
weight: 4,
|
|
opacity: 0.8
|
|
},
|
|
...options
|
|
};
|
|
|
|
this.container = null;
|
|
this.mapInstance = null;
|
|
this.selectedParks = [];
|
|
this.routeLayer = null;
|
|
this.parkMarkers = new Map();
|
|
this.routePolyline = null;
|
|
this.routeData = null;
|
|
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* Initialize the road trip planner
|
|
*/
|
|
init() {
|
|
this.container = document.getElementById(this.containerId);
|
|
if (!this.container) {
|
|
console.error(`Road trip container with ID '${this.containerId}' not found`);
|
|
return;
|
|
}
|
|
|
|
this.setupUI();
|
|
this.bindEvents();
|
|
|
|
// Connect to map instance if provided
|
|
if (this.options.mapInstance) {
|
|
this.connectToMap(this.options.mapInstance);
|
|
}
|
|
|
|
this.loadInitialData();
|
|
}
|
|
|
|
/**
|
|
* Setup the UI components
|
|
*/
|
|
setupUI() {
|
|
const html = `
|
|
<div class="roadtrip-planner">
|
|
<div class="roadtrip-header">
|
|
<h3 class="roadtrip-title">
|
|
<i class="fas fa-route"></i>
|
|
Road Trip Planner
|
|
</h3>
|
|
<div class="roadtrip-controls">
|
|
<button id="optimize-route" class="btn btn-secondary btn-sm" disabled>
|
|
<i class="fas fa-magic"></i> Optimize Route
|
|
</button>
|
|
<button id="clear-route" class="btn btn-outline btn-sm" disabled>
|
|
<i class="fas fa-trash"></i> Clear All
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="roadtrip-content">
|
|
<div class="park-selection">
|
|
<div class="search-parks">
|
|
<input type="text" id="park-search"
|
|
placeholder="Search parks to add..."
|
|
class="form-input">
|
|
<div id="park-search-results" class="search-results"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="selected-parks">
|
|
<h4 class="section-title">Your Route (<span id="park-count">0</span>/${this.options.maxParks})</h4>
|
|
<div id="parks-list" class="parks-list sortable">
|
|
<div class="empty-state">
|
|
<i class="fas fa-map-marked-alt"></i>
|
|
<p>Search and select parks to build your road trip route</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="route-summary" id="route-summary" style="display: none;">
|
|
<h4 class="section-title">Trip Summary</h4>
|
|
<div class="summary-stats">
|
|
<div class="stat">
|
|
<span class="stat-label">Total Distance:</span>
|
|
<span id="total-distance" class="stat-value">-</span>
|
|
</div>
|
|
<div class="stat">
|
|
<span class="stat-label">Driving Time:</span>
|
|
<span id="total-time" class="stat-value">-</span>
|
|
</div>
|
|
<div class="stat">
|
|
<span class="stat-label">Parks:</span>
|
|
<span id="total-parks" class="stat-value">0</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="export-options">
|
|
<button id="export-gpx" class="btn btn-outline btn-sm">
|
|
<i class="fas fa-download"></i> Export GPX
|
|
</button>
|
|
<button id="export-kml" class="btn btn-outline btn-sm">
|
|
<i class="fas fa-download"></i> Export KML
|
|
</button>
|
|
<button id="share-route" class="btn btn-primary btn-sm">
|
|
<i class="fas fa-share"></i> Share Route
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
this.container.innerHTML = html;
|
|
}
|
|
|
|
/**
|
|
* Bind event handlers
|
|
*/
|
|
bindEvents() {
|
|
// Park search
|
|
const searchInput = document.getElementById('park-search');
|
|
if (searchInput) {
|
|
let searchTimeout;
|
|
searchInput.addEventListener('input', (e) => {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(() => {
|
|
this.searchParks(e.target.value);
|
|
}, 300);
|
|
});
|
|
}
|
|
|
|
// Route controls
|
|
const optimizeBtn = document.getElementById('optimize-route');
|
|
if (optimizeBtn) {
|
|
optimizeBtn.addEventListener('click', () => this.optimizeRoute());
|
|
}
|
|
|
|
const clearBtn = document.getElementById('clear-route');
|
|
if (clearBtn) {
|
|
clearBtn.addEventListener('click', () => this.clearRoute());
|
|
}
|
|
|
|
// Export buttons
|
|
const exportGpxBtn = document.getElementById('export-gpx');
|
|
if (exportGpxBtn) {
|
|
exportGpxBtn.addEventListener('click', () => this.exportRoute('gpx'));
|
|
}
|
|
|
|
const exportKmlBtn = document.getElementById('export-kml');
|
|
if (exportKmlBtn) {
|
|
exportKmlBtn.addEventListener('click', () => this.exportRoute('kml'));
|
|
}
|
|
|
|
const shareBtn = document.getElementById('share-route');
|
|
if (shareBtn) {
|
|
shareBtn.addEventListener('click', () => this.shareRoute());
|
|
}
|
|
|
|
// Make parks list sortable
|
|
this.initializeSortable();
|
|
}
|
|
|
|
/**
|
|
* Initialize drag-and-drop sorting for parks list
|
|
*/
|
|
initializeSortable() {
|
|
const parksList = document.getElementById('parks-list');
|
|
if (!parksList) return;
|
|
|
|
// Simple drag and drop implementation
|
|
let draggedElement = null;
|
|
|
|
parksList.addEventListener('dragstart', (e) => {
|
|
if (e.target.classList.contains('park-item')) {
|
|
draggedElement = e.target;
|
|
e.target.style.opacity = '0.5';
|
|
}
|
|
});
|
|
|
|
parksList.addEventListener('dragend', (e) => {
|
|
if (e.target.classList.contains('park-item')) {
|
|
e.target.style.opacity = '1';
|
|
draggedElement = null;
|
|
}
|
|
});
|
|
|
|
parksList.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
});
|
|
|
|
parksList.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
|
|
if (draggedElement && e.target.classList.contains('park-item')) {
|
|
const afterElement = this.getDragAfterElement(parksList, e.clientY);
|
|
|
|
if (afterElement == null) {
|
|
parksList.appendChild(draggedElement);
|
|
} else {
|
|
parksList.insertBefore(draggedElement, afterElement);
|
|
}
|
|
|
|
this.reorderParks();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the element to insert after during drag and drop
|
|
*/
|
|
getDragAfterElement(container, y) {
|
|
const draggableElements = [...container.querySelectorAll('.park-item:not(.dragging)')];
|
|
|
|
return draggableElements.reduce((closest, child) => {
|
|
const box = child.getBoundingClientRect();
|
|
const offset = y - box.top - box.height / 2;
|
|
|
|
if (offset < 0 && offset > closest.offset) {
|
|
return { offset: offset, element: child };
|
|
} else {
|
|
return closest;
|
|
}
|
|
}, { offset: Number.NEGATIVE_INFINITY }).element;
|
|
}
|
|
|
|
/**
|
|
* Search for parks
|
|
*/
|
|
async searchParks(query) {
|
|
if (!query.trim()) {
|
|
document.getElementById('park-search-results').innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${this.options.apiEndpoints.parks}?q=${encodeURIComponent(query)}&limit=10`);
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'success') {
|
|
this.displaySearchResults(data.data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to search parks:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display park search results
|
|
*/
|
|
displaySearchResults(parks) {
|
|
const resultsContainer = document.getElementById('park-search-results');
|
|
|
|
if (parks.length === 0) {
|
|
resultsContainer.innerHTML = '<div class="no-results">No parks found</div>';
|
|
return;
|
|
}
|
|
|
|
const html = parks
|
|
.filter(park => !this.isParkSelected(park.id))
|
|
.map(park => `
|
|
<div class="search-result-item" data-park-id="${park.id}">
|
|
<div class="park-info">
|
|
<div class="park-name">${park.name}</div>
|
|
<div class="park-location">${park.formatted_location || ''}</div>
|
|
</div>
|
|
<button class="add-park-btn" onclick="roadTripPlanner.addPark(${park.id})">
|
|
<i class="fas fa-plus"></i>
|
|
</button>
|
|
</div>
|
|
`).join('');
|
|
|
|
resultsContainer.innerHTML = html;
|
|
}
|
|
|
|
/**
|
|
* Check if a park is already selected
|
|
*/
|
|
isParkSelected(parkId) {
|
|
return this.selectedParks.some(park => park.id === parkId);
|
|
}
|
|
|
|
/**
|
|
* Add a park to the route
|
|
*/
|
|
async addPark(parkId) {
|
|
if (this.selectedParks.length >= this.options.maxParks) {
|
|
this.showMessage(`Maximum ${this.options.maxParks} parks allowed`, 'warning');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${this.options.apiEndpoints.parks}${parkId}/`);
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'success') {
|
|
const park = data.data;
|
|
this.selectedParks.push(park);
|
|
this.updateParksDisplay();
|
|
this.addParkMarker(park);
|
|
this.updateRoute();
|
|
|
|
// Clear search
|
|
document.getElementById('park-search').value = '';
|
|
document.getElementById('park-search-results').innerHTML = '';
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to add park:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove a park from the route
|
|
*/
|
|
removePark(parkId) {
|
|
const index = this.selectedParks.findIndex(park => park.id === parkId);
|
|
if (index > -1) {
|
|
this.selectedParks.splice(index, 1);
|
|
this.updateParksDisplay();
|
|
this.removeParkMarker(parkId);
|
|
this.updateRoute();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the parks display
|
|
*/
|
|
updateParksDisplay() {
|
|
const parksList = document.getElementById('parks-list');
|
|
const parkCount = document.getElementById('park-count');
|
|
|
|
parkCount.textContent = this.selectedParks.length;
|
|
|
|
if (this.selectedParks.length === 0) {
|
|
parksList.innerHTML = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-map-marked-alt"></i>
|
|
<p>Search and select parks to build your road trip route</p>
|
|
</div>
|
|
`;
|
|
this.updateControls();
|
|
return;
|
|
}
|
|
|
|
const html = this.selectedParks.map((park, index) => `
|
|
<div class="park-item" draggable="true" data-park-id="${park.id}">
|
|
<div class="park-number">${index + 1}</div>
|
|
<div class="park-details">
|
|
<div class="park-name">${park.name}</div>
|
|
<div class="park-location">${park.formatted_location || ''}</div>
|
|
${park.distance_from_previous ? `<div class="park-distance">${park.distance_from_previous}</div>` : ''}
|
|
</div>
|
|
<div class="park-actions">
|
|
<button class="btn-icon" onclick="roadTripPlanner.removePark(${park.id})" title="Remove park">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
parksList.innerHTML = html;
|
|
this.updateControls();
|
|
}
|
|
|
|
/**
|
|
* Update control buttons state
|
|
*/
|
|
updateControls() {
|
|
const optimizeBtn = document.getElementById('optimize-route');
|
|
const clearBtn = document.getElementById('clear-route');
|
|
|
|
const hasParks = this.selectedParks.length > 0;
|
|
const canOptimize = this.selectedParks.length > 2;
|
|
|
|
if (optimizeBtn) optimizeBtn.disabled = !canOptimize;
|
|
if (clearBtn) clearBtn.disabled = !hasParks;
|
|
}
|
|
|
|
/**
|
|
* Reorder parks after drag and drop
|
|
*/
|
|
reorderParks() {
|
|
const parkItems = document.querySelectorAll('.park-item');
|
|
const newOrder = [];
|
|
|
|
parkItems.forEach(item => {
|
|
const parkId = parseInt(item.dataset.parkId);
|
|
const park = this.selectedParks.find(p => p.id === parkId);
|
|
if (park) {
|
|
newOrder.push(park);
|
|
}
|
|
});
|
|
|
|
this.selectedParks = newOrder;
|
|
this.updateRoute();
|
|
}
|
|
|
|
/**
|
|
* Update the route visualization
|
|
*/
|
|
async updateRoute() {
|
|
if (this.selectedParks.length < 2) {
|
|
this.clearRouteVisualization();
|
|
this.updateRouteSummary(null);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const parkIds = this.selectedParks.map(park => park.id);
|
|
const response = await fetch(`${this.options.apiEndpoints.route}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': this.getCsrfToken()
|
|
},
|
|
body: JSON.stringify({ parks: parkIds })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'success') {
|
|
this.routeData = data.data;
|
|
this.visualizeRoute(data.data);
|
|
this.updateRouteSummary(data.data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to calculate route:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Visualize the route on the map
|
|
*/
|
|
visualizeRoute(routeData) {
|
|
if (!this.mapInstance) return;
|
|
|
|
// Clear existing route
|
|
this.clearRouteVisualization();
|
|
|
|
if (routeData.coordinates) {
|
|
// Create polyline from coordinates
|
|
this.routePolyline = L.polyline(routeData.coordinates, this.options.routeOptions);
|
|
this.routePolyline.addTo(this.mapInstance);
|
|
|
|
// Fit map to route bounds
|
|
if (routeData.coordinates.length > 0) {
|
|
this.mapInstance.fitBounds(this.routePolyline.getBounds(), { padding: [20, 20] });
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear route visualization
|
|
*/
|
|
clearRouteVisualization() {
|
|
if (this.routePolyline && this.mapInstance) {
|
|
this.mapInstance.removeLayer(this.routePolyline);
|
|
this.routePolyline = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update route summary display
|
|
*/
|
|
updateRouteSummary(routeData) {
|
|
const summarySection = document.getElementById('route-summary');
|
|
|
|
if (!routeData || this.selectedParks.length < 2) {
|
|
summarySection.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
summarySection.style.display = 'block';
|
|
|
|
document.getElementById('total-distance').textContent = routeData.total_distance || '-';
|
|
document.getElementById('total-time').textContent = routeData.total_time || '-';
|
|
document.getElementById('total-parks').textContent = this.selectedParks.length;
|
|
}
|
|
|
|
/**
|
|
* Optimize the route order
|
|
*/
|
|
async optimizeRoute() {
|
|
if (this.selectedParks.length < 3) return;
|
|
|
|
try {
|
|
const parkIds = this.selectedParks.map(park => park.id);
|
|
const response = await fetch(`${this.options.apiEndpoints.optimize}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': this.getCsrfToken()
|
|
},
|
|
body: JSON.stringify({ parks: parkIds })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'success') {
|
|
// Reorder parks based on optimization
|
|
const optimizedOrder = data.data.optimized_order;
|
|
this.selectedParks = optimizedOrder.map(id =>
|
|
this.selectedParks.find(park => park.id === id)
|
|
).filter(Boolean);
|
|
|
|
this.updateParksDisplay();
|
|
this.updateRoute();
|
|
this.showMessage('Route optimized for shortest distance', 'success');
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to optimize route:', error);
|
|
this.showMessage('Failed to optimize route', 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear the entire route
|
|
*/
|
|
clearRoute() {
|
|
this.selectedParks = [];
|
|
this.clearAllParkMarkers();
|
|
this.clearRouteVisualization();
|
|
this.updateParksDisplay();
|
|
this.updateRouteSummary(null);
|
|
}
|
|
|
|
/**
|
|
* Export route in specified format
|
|
*/
|
|
async exportRoute(format) {
|
|
if (!this.routeData) {
|
|
this.showMessage('No route to export', 'warning');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${this.options.apiEndpoints.export}${format}/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': this.getCsrfToken()
|
|
},
|
|
body: JSON.stringify({
|
|
parks: this.selectedParks.map(p => p.id),
|
|
route_data: this.routeData
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `thrillwiki-roadtrip.${format}`;
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to export route:', error);
|
|
this.showMessage('Failed to export route', 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Share the route
|
|
*/
|
|
shareRoute() {
|
|
if (this.selectedParks.length === 0) {
|
|
this.showMessage('No route to share', 'warning');
|
|
return;
|
|
}
|
|
|
|
const parkIds = this.selectedParks.map(p => p.id).join(',');
|
|
const url = `${window.location.origin}/roadtrip/?parks=${parkIds}`;
|
|
|
|
if (navigator.share) {
|
|
navigator.share({
|
|
title: 'ThrillWiki Road Trip',
|
|
text: `Check out this ${this.selectedParks.length}-park road trip!`,
|
|
url: url
|
|
});
|
|
} else {
|
|
// Fallback to clipboard
|
|
navigator.clipboard.writeText(url).then(() => {
|
|
this.showMessage('Route URL copied to clipboard', 'success');
|
|
}).catch(() => {
|
|
// Manual selection fallback
|
|
const textarea = document.createElement('textarea');
|
|
textarea.value = url;
|
|
document.body.appendChild(textarea);
|
|
textarea.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(textarea);
|
|
this.showMessage('Route URL copied to clipboard', 'success');
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add park marker to map
|
|
*/
|
|
addParkMarker(park) {
|
|
if (!this.mapInstance) return;
|
|
|
|
const marker = L.marker([park.latitude, park.longitude], {
|
|
icon: this.createParkIcon(park)
|
|
});
|
|
|
|
marker.bindPopup(`
|
|
<div class="park-popup">
|
|
<h4>${park.name}</h4>
|
|
<p>${park.formatted_location || ''}</p>
|
|
<button onclick="roadTripPlanner.removePark(${park.id})" class="btn btn-sm btn-outline">
|
|
Remove from Route
|
|
</button>
|
|
</div>
|
|
`);
|
|
|
|
marker.addTo(this.mapInstance);
|
|
this.parkMarkers.set(park.id, marker);
|
|
}
|
|
|
|
/**
|
|
* Remove park marker from map
|
|
*/
|
|
removeParkMarker(parkId) {
|
|
if (this.parkMarkers.has(parkId) && this.mapInstance) {
|
|
this.mapInstance.removeLayer(this.parkMarkers.get(parkId));
|
|
this.parkMarkers.delete(parkId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear all park markers
|
|
*/
|
|
clearAllParkMarkers() {
|
|
this.parkMarkers.forEach(marker => {
|
|
if (this.mapInstance) {
|
|
this.mapInstance.removeLayer(marker);
|
|
}
|
|
});
|
|
this.parkMarkers.clear();
|
|
}
|
|
|
|
/**
|
|
* Create custom icon for park marker
|
|
*/
|
|
createParkIcon(park) {
|
|
const index = this.selectedParks.findIndex(p => p.id === park.id) + 1;
|
|
|
|
return L.divIcon({
|
|
className: 'roadtrip-park-marker',
|
|
html: `<div class="park-marker-inner">${index}</div>`,
|
|
iconSize: [30, 30],
|
|
iconAnchor: [15, 15]
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Connect to a map instance
|
|
*/
|
|
connectToMap(mapInstance) {
|
|
this.mapInstance = mapInstance;
|
|
this.options.mapInstance = mapInstance;
|
|
}
|
|
|
|
/**
|
|
* Load initial data (from URL parameters)
|
|
*/
|
|
loadInitialData() {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const parkIds = urlParams.get('parks');
|
|
|
|
if (parkIds) {
|
|
const ids = parkIds.split(',').map(id => parseInt(id)).filter(id => !isNaN(id));
|
|
this.loadParksById(ids);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load parks by IDs
|
|
*/
|
|
async loadParksById(parkIds) {
|
|
try {
|
|
const promises = parkIds.map(id =>
|
|
fetch(`${this.options.apiEndpoints.parks}${id}/`)
|
|
.then(res => res.json())
|
|
.then(data => data.status === 'success' ? data.data : null)
|
|
);
|
|
|
|
const parks = (await Promise.all(promises)).filter(Boolean);
|
|
|
|
this.selectedParks = parks;
|
|
this.updateParksDisplay();
|
|
|
|
// Add markers and update route
|
|
parks.forEach(park => this.addParkMarker(park));
|
|
this.updateRoute();
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load parks:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get CSRF token for POST requests
|
|
*/
|
|
getCsrfToken() {
|
|
const token = document.querySelector('[name=csrfmiddlewaretoken]');
|
|
return token ? token.value : '';
|
|
}
|
|
|
|
/**
|
|
* Show message to user
|
|
*/
|
|
showMessage(message, type = 'info') {
|
|
// Create or update message element
|
|
let messageEl = this.container.querySelector('.roadtrip-message');
|
|
if (!messageEl) {
|
|
messageEl = document.createElement('div');
|
|
messageEl.className = 'roadtrip-message';
|
|
this.container.insertBefore(messageEl, this.container.firstChild);
|
|
}
|
|
|
|
messageEl.textContent = message;
|
|
messageEl.className = `roadtrip-message roadtrip-message-${type}`;
|
|
|
|
// Auto-hide after delay
|
|
setTimeout(() => {
|
|
if (messageEl.parentNode) {
|
|
messageEl.remove();
|
|
}
|
|
}, 5000);
|
|
}
|
|
}
|
|
|
|
// Auto-initialize road trip planner
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const roadtripContainer = document.getElementById('roadtrip-planner');
|
|
if (roadtripContainer) {
|
|
window.roadTripPlanner = new RoadTripPlanner('roadtrip-planner', {
|
|
mapInstance: window.thrillwikiMap || null
|
|
});
|
|
}
|
|
});
|
|
|
|
// Export for use in other modules
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = RoadTripPlanner;
|
|
} else {
|
|
window.RoadTripPlanner = RoadTripPlanner;
|
|
} |