// Guest-mode local GPX viewer β€” no backend, no auth required. const LocalViewer = (() => { let tracks = {}; // id β†’ { data: {segments,meta}, visible: bool } let nextId = 0; let currentId = null; // track shown in info panel / elevation chart // ===== Public ===== function init() { setupSidebar(); setupMapDrop(); } // ===== Sidebar ===== function setupSidebar() { // Replace logged-in actions with a simple "Open GPX" button document.getElementById('browser-actions').innerHTML = ` `; document.getElementById('local-open-btn').addEventListener('click', () => { document.getElementById('local-file-input').click(); }); document.getElementById('local-file-input').addEventListener('change', (e) => { handleFiles(Array.from(e.target.files)); e.target.value = ''; }); // Clear breadcrumb β€” not relevant in guest mode document.getElementById('breadcrumb').innerHTML = 'Local viewer'; renderList(); } // ===== Map drop zone ===== function setupMapDrop() { const mc = document.getElementById('map-container'); mc.addEventListener('dragover', (e) => { if (e.dataTransfer.types.includes('Files')) { e.preventDefault(); mc.classList.add('drag-over'); } }); mc.addEventListener('dragleave', (e) => { if (!mc.contains(e.relatedTarget)) mc.classList.remove('drag-over'); }); mc.addEventListener('drop', (e) => { e.preventDefault(); mc.classList.remove('drag-over'); const files = Array.from(e.dataTransfer.files) .filter(f => f.name.toLowerCase().endsWith('.gpx')); if (files.length) handleFiles(files); }); } // ===== File loading ===== async function handleFiles(files) { for (const file of files) { try { const text = await file.text(); const data = parseGPX(text, file.name); const id = 'local_' + (nextId++); tracks[id] = { data, visible: false }; toggleTrack(id); // open immediately } catch (e) { showToast('Error reading ' + file.name + ': ' + e.message, 'error'); } } } // ===== Track toggling ===== function toggleTrack(id) { const entry = tracks[id]; if (!entry) return; if (entry.visible) { MapView.removeTrack(id); entry.visible = false; if (currentId === id) { currentId = null; MapView.setCurrentTrack(null); document.getElementById('track-info-panel').style.display = 'none'; if (typeof Elevation !== 'undefined') Elevation.clear(); } } else { MapView.addTrack(entry.data, id); MapView.fitTrack(id); MapView.setCurrentTrack(id); entry.visible = true; currentId = id; showTrackInfo(entry.data.meta); if (typeof Elevation !== 'undefined') { const pts = MapView.getTrackPoints(id); if (pts) Elevation.setTrack(pts); } } renderList(); } function removeTrack(id) { if (tracks[id]?.visible) MapView.removeTrack(id); if (currentId === id) { currentId = null; MapView.setCurrentTrack(null); document.getElementById('track-info-panel').style.display = 'none'; if (typeof Elevation !== 'undefined') Elevation.clear(); } delete tracks[id]; renderList(); } // ===== Rendering ===== function renderList() { const list = document.getElementById('browser-list'); const ids = Object.keys(tracks); if (ids.length === 0) { list.innerHTML = '
Open a GPX file or drop it onto the map.
'; return; } let html = ''; for (const id of ids) { const { data, visible } = tracks[id]; const meta = data.meta; const dist = meta.totalDistance ? formatDistance(meta.totalDistance) : ''; html += `
πŸ—ΊοΈ ${escHtml(meta.name)} ${dist}
`; } list.innerHTML = html; list.querySelectorAll('.track-item').forEach(el => { el.addEventListener('click', (e) => { if (e.target.closest('.item-actions')) return; toggleTrack(el.dataset.id); }); }); list.querySelectorAll('.local-remove-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); removeTrack(btn.dataset.id); }); }); } // ===== Track info panel ===== function showTrackInfo(meta) { if (!meta) return; document.getElementById('track-info-content').innerHTML = `

${escHtml(meta.name || 'Track')}

${formatDistance(meta.totalDistance)}
${meta.pointCount || 0}
${meta.trackDate ? `
${formatDate(meta.trackDate)}
` : ''} `; const panel = document.getElementById('track-info-panel'); panel.style.display = 'block'; document.getElementById('track-info-close').onclick = () => { panel.style.display = 'none'; }; } // ===== GPX parsing ===== function haversine(lat1, lon1, lat2, lon2) { const R = 6371000; const dLat = (lat2 - lat1) * Math.PI / 180; const dLon = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) ** 2; return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } function parseGPX(xmlText, filename) { const doc = new DOMParser().parseFromString(xmlText, 'application/xml'); if (doc.querySelector('parsererror')) throw new Error('Invalid GPX XML'); const nameNode = doc.querySelector('trk > name, rte > name, metadata > name'); const name = nameNode ? nameNode.textContent.trim() : filename.replace(/\.gpx$/i, ''); const segments = []; let trackDate = null; // Standard tracks (trkseg/trkpt) for (const seg of doc.querySelectorAll('trkseg')) { const pts = parsePts(seg.querySelectorAll('trkpt')); if (pts.length) { if (!trackDate && pts[0][3]) trackDate = pts[0][3]; segments.push(pts); } } // Routes as fallback (rte/rtept) if (segments.length === 0) { for (const rte of doc.querySelectorAll('rte')) { const pts = parsePts(rte.querySelectorAll('rtept')); if (pts.length) segments.push(pts); } } if (segments.length === 0) throw new Error('No track points found'); // Compute totals let totalDistance = 0; let pointCount = 0; for (const seg of segments) { pointCount += seg.length; for (let i = 1; i < seg.length; i++) { totalDistance += haversine(seg[i - 1][0], seg[i - 1][1], seg[i][0], seg[i][1]); } } return { segments, meta: { name, totalDistance, pointCount, trackDate } }; } function parsePts(nodeList) { const pts = []; for (const pt of nodeList) { const lat = parseFloat(pt.getAttribute('lat')); const lon = parseFloat(pt.getAttribute('lon')); if (isNaN(lat) || isNaN(lon)) continue; const eleNode = pt.querySelector('ele'); const timeNode = pt.querySelector('time'); const ele = eleNode ? parseFloat(eleNode.textContent) : null; const time = timeNode ? timeNode.textContent : null; pts.push([lat, lon, (ele != null && !isNaN(ele)) ? ele : null, time]); } return pts; } return { init }; })();