/* Minimal Roadtrip JS helpers for HTMX-driven planner - Initializes map helpers when Leaflet is available - Exposes `RoadtripMap` global with basic marker helpers - Heavy client-side trip logic is intentionally moved to HTMX endpoints */ class RoadtripMap { constructor() { this.map = null; this.markers = {}; } init(containerId, opts = {}) { if (typeof L === 'undefined') return; try { this.map = L.map(containerId).setView([51.505, -0.09], 5); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(this.map); } catch (e) { console.error('Failed to initialize map', e); } } addMarker(park) { if (!this.map || !park || !park.latitude || !park.longitude) return; const id = park.id; if (this.markers[id]) return; const m = L.marker([park.latitude, park.longitude]).addTo(this.map).bindPopup(park.name); this.markers[id] = m; } removeMarker(parkId) { const m = this.markers[parkId]; if (m && this.map) { this.map.removeLayer(m); delete this.markers[parkId]; } } fitToMarkers() { const keys = Object.keys(this.markers); if (!this.map || keys.length === 0) return; const group = new L.featureGroup(keys.map(k => this.markers[k])); this.map.fitBounds(group.getBounds().pad(0.2)); } showRoute(orderedParks = []) { if (!this.map || typeof L.Routing === 'undefined') return; // remove existing control if present if (this._routingControl) { try { this.map.removeControl(this._routingControl); } catch (e) {} this._routingControl = null; } const waypoints = orderedParks .filter(p => p.latitude && p.longitude) .map(p => L.latLng(p.latitude, p.longitude)); if (waypoints.length < 2) return; try { this._routingControl = L.Routing.control({ waypoints: waypoints, draggableWaypoints: false, addWaypoints: false, showAlternatives: false, routeWhileDragging: false, fitSelectedRoute: true, createMarker: function(i, wp) { const cls = i === 0 ? 'waypoint-start' : (i === waypoints.length - 1 ? 'waypoint-end' : 'waypoint-stop'); return L.marker(wp.latLng, { className: 'waypoint-marker ' + cls }).bindPopup(`Stop ${i+1}`); } }).addTo(this.map); } catch (e) { console.error('Routing error', e); } } } // Expose simple global for templates to call globalThis.RoadtripMap = new RoadtripMap(); // Backwards-compatible lightweight planner shim used by other scripts class RoadTripPlannerShim { constructor(containerId) { this.containerId = containerId; } async addPark(parkId) { // POST to HTMX add endpoint and insert returned fragment try { const csrftoken = (document.cookie.match(/(^|;)\s*csrftoken=([^;]+)/) || [])[2]; const resp = await fetch(`/parks/roadtrip/htmx/add-park/`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': csrftoken || '' }, body: `park_id=${encodeURIComponent(parkId)}`, credentials: 'same-origin' }); const html = await resp.text(); const container = document.getElementById('trip-parks'); if (container) container.insertAdjacentHTML('afterbegin', html); if (globalThis.RoadtripMap && typeof globalThis.RoadtripMap.addMarker === 'function') { try { const parkResp = await fetch(`/api/parks/${parkId}/`); const parkJson = await parkResp.json(); if (parkJson && parkJson.data) globalThis.RoadtripMap.addMarker(parkJson.data); } catch (e) { // ignore } } } catch (e) { console.error('Failed to add park via HTMX shim', e); } } removePark(parkId) { const el = document.querySelector(`[data-park-id="${parkId}"]`); if (el) el.remove(); if (globalThis.RoadtripMap && typeof globalThis.RoadtripMap.removeMarker === 'function') { globalThis.RoadtripMap.removeMarker(parkId); } } fitRoute() { if (globalThis.RoadtripMap && typeof globalThis.RoadtripMap.fitToMarkers === 'function') { globalThis.RoadtripMap.fitToMarkers(); } } toggleAllParks() { // No-op in shim; map integration can implement this separately console.debug('toggleAllParks called (shim)'); } } // Expose compatibility globals globalThis.RoadTripPlanner = RoadTripPlannerShim; document.addEventListener('DOMContentLoaded', function() { try { globalThis.roadTripPlanner = new RoadTripPlannerShim('roadtrip-planner'); } catch (e) { // ignore } }); // Initialize Sortable for #trip-parks and POST new order to server document.addEventListener('DOMContentLoaded', function () { try { if (typeof Sortable === 'undefined') return; const el = document.getElementById('trip-parks'); if (!el) return; // avoid double-init if (el._sortableInit) return; el._sortableInit = true; function getCookie(name) { const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); if (match) return decodeURIComponent(match[2]); return null; } new Sortable(el, { animation: 150, ghostClass: 'drag-over', handle: '.draggable-item', onEnd: function (evt) { // gather order from container children const order = Array.from(el.children).map(function (c) { return c.dataset.parkId; }).filter(Boolean); const csrftoken = getCookie('csrftoken'); fetch('/parks/roadtrip/htmx/reorder/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrftoken || '' }, credentials: 'same-origin', body: JSON.stringify({ order: order }) }).then(function (r) { return r.text(); }).then(function (html) { // replace inner HTML with server-rendered partial el.innerHTML = html; // notify other listeners (map, summary) document.dispatchEvent(new CustomEvent('tripReordered', { detail: { order: order } })); }).catch(function (err) { console.error('Failed to post reorder', err); }); } }); } catch (e) { console.error('Sortable init error', e); } }); // Listen for HTMX trigger event and show route when available document.addEventListener('tripOptimized', function (ev) { try { const payload = ev && ev.detail ? ev.detail : {}; const parks = (payload && payload.parks) || []; if (globalThis.RoadtripMap && typeof globalThis.RoadtripMap.showRoute === 'function') { globalThis.RoadtripMap.showRoute(parks); } } catch (e) { // ignore } }); // End of roadtrip helpers