| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- // 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 = `
- <button id="local-open-btn" class="action-btn">Open GPX</button>
- <input type="file" id="local-file-input" accept=".gpx" multiple style="display:none">
- `;
- 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 =
- '<span style="color:var(--color-text-lighter);font-size:12px">Local viewer</span>';
- 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 = '<div class="empty-list">Open a GPX file or drop it onto the map.</div>';
- 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 += `<div class="tree-item track-item${visible ? ' local-active' : ''}"
- data-id="${escAttr(id)}" style="padding-left:12px">
- <span class="item-icon">🗺️</span>
- <span class="item-name">${escHtml(meta.name)}</span>
- <span class="item-meta">${dist}</span>
- <span class="item-actions">
- <button class="item-btn local-remove-btn" data-id="${escAttr(id)}" title="Remove">🗑️</button>
- </span>
- </div>`;
- }
- 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 = `
- <h3>${escHtml(meta.name || 'Track')}</h3>
- <div class="info-row"><label>Distance</label><span>${formatDistance(meta.totalDistance)}</span></div>
- <div class="info-row"><label>Points</label><span>${meta.pointCount || 0}</span></div>
- ${meta.trackDate ? `<div class="info-row"><label>Date</label><span>${formatDate(meta.trackDate)}</span></div>` : ''}
- `;
- 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 };
- })();
|