/* ============================================================ BAN YARO — Gassi-Routen Routen entdecken (Karte + Liste) + GPS-Aufzeichnung ============================================================ */ window.Page_routes = (() => { let _container = null; let _appState = null; let _map = null; let _polylines = []; let _data = []; let _activeTab = 'entdecken'; // 'entdecken' | 'aufzeichnen' let _leafletLoaded = false; let _userPos = null; // Aufzeichnung let _recording = false; let _watchId = null; let _track = []; // [{lat, lon}] let _distanceKm = 0; let _startTime = null; let _timerInt = null; let _recPolyline = null; let _recMarker = null; const SCHWIERIGKEIT = ['leicht', 'mittel', 'anspruchsvoll']; const UNTERGRUND = { wald: '🌲 Wald', asphalt: '🛣️ Asphalt', wiese: '🌿 Wiese', mix: '🔀 Mix' }; function _esc(s) { return String(s || '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function _haversine(lat1, lon1, lat2, lon2) { const R = 6371000; const p1 = lat1 * Math.PI / 180, p2 = lat2 * Math.PI / 180; const dp = (lat2 - lat1) * Math.PI / 180; const dl = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dp/2)**2 + Math.cos(p1)*Math.cos(p2)*Math.sin(dl/2)**2; return 2 * R * Math.asin(Math.sqrt(a)); } // ---------------------------------------------------------- // INIT // ---------------------------------------------------------- async function init(container, appState) { _container = container; _appState = appState; _render(); _loadData(); try { _userPos = await API.getLocation(); } catch {} } function refresh() { _loadData(); } function onDogChange() {} // ---------------------------------------------------------- // RENDER — Grundstruktur // ---------------------------------------------------------- function _render() { _container.innerHTML = `
Lädt…
Noch keine Routen vorhanden.
${_esc(route.beschreibung)}
` : ''}${track.length} GPS-Punkte · von ${_esc(route.user_name || 'Unbekannt')}
`; const footer = isOwn ? ` ` : ` `; UI.modal.open({ title: `🥾 ${route.name}`, body, footer }); document.getElementById('rd-close')?.addEventListener('click', UI.modal.close); document.getElementById('rd-delete')?.addEventListener('click', async () => { const ok = await UI.modal.confirm({ title: 'Route löschen?', message: `„${route.name}" wird dauerhaft entfernt.`, confirmText: 'Löschen', danger: true, }); if (!ok) return; try { await API.routes.delete(route.id); _data = _data.filter(r => r.id !== route.id); UI.modal.close(); _renderList(); _renderPolylines(); UI.toast.success('Route gelöscht.'); } catch (err) { UI.toast.error(err.message); } }); // Mini-Map mit Polyline nach DOM-Einfüge setTimeout(() => { const el = document.getElementById('route-detail-map'); if (!el || !window.L || !track.length) return; const detailMap = L.map('route-detail-map', { zoomControl: false, attributionControl: false }); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(detailMap); const latlngs = track.map(p => [p.lat, p.lon]); const polyline = L.polyline(latlngs, { color: '#C4843A', weight: 4, opacity: 0.85 }).addTo(detailMap); // Start/End-Marker L.circleMarker(latlngs[0], { radius: 7, color: '#22C55E', fillColor: '#22C55E', fillOpacity: 1 }).addTo(detailMap); L.circleMarker(latlngs[latlngs.length-1], { radius: 7, color: '#EF4444', fillColor: '#EF4444', fillOpacity: 1 }).addTo(detailMap); detailMap.fitBounds(polyline.getBounds(), { padding: [10, 10] }); }, 100); } // ---------------------------------------------------------- // GPS-AUFZEICHNUNG // ---------------------------------------------------------- function _startRecording() { if (!_appState.user) { UI.toast.warning('Bitte zuerst anmelden.'); App.navigate('settings'); return; } if (!navigator.geolocation) { UI.toast.error('GPS wird von diesem Browser nicht unterstützt.'); return; } _recording = true; _track = []; _distanceKm = 0; _startTime = Date.now(); // UI umschalten document.getElementById('rec-idle').style.display = 'none'; document.getElementById('rec-active').style.display = ''; document.getElementById('rec-stats').style.display = ''; // Stopuhr _timerInt = setInterval(_updateRecTimer, 1000); // GPS-Tracking _watchId = navigator.geolocation.watchPosition( pos => _onGpsPoint(pos.coords.latitude, pos.coords.longitude), err => UI.toast.warning('GPS-Fehler: ' + err.message), { enableHighAccuracy: true, maximumAge: 0, timeout: 15000 } ); // Buttons binden document.getElementById('rec-stop-btn').onclick = _stopRecording; document.getElementById('rec-pause-btn').onclick = _togglePause; UI.toast.success('Aufzeichnung gestartet — los geht\'s!'); } let _paused = false; function _togglePause() { _paused = !_paused; const btn = document.getElementById('rec-pause-btn'); if (btn) btn.textContent = _paused ? '▶ Weiter' : '⏸ Pause'; if (_paused && _timerInt) clearInterval(_timerInt); if (!_paused) _timerInt = setInterval(_updateRecTimer, 1000); } function _onGpsPoint(lat, lon) { if (_paused) return; const pt = { lat, lon }; if (_track.length > 0) { const prev = _track[_track.length - 1]; _distanceKm += _haversine(prev.lat, prev.lon, lat, lon) / 1000; } _track.push(pt); _updateRecStats(); _updateRecMap(lat, lon); } function _updateRecMap(lat, lon) { if (!_recMap || !window.L) return; const latlng = [lat, lon]; if (!_recPolyline) { _recPolyline = L.polyline([latlng], { color: '#EF4444', weight: 5, opacity: 0.9 }).addTo(_recMap); } else { _recPolyline.addLatLng(latlng); } if (!_recMarker) { _recMarker = L.circleMarker(latlng, { radius: 8, color: '#EF4444', fillColor: '#fff', fillOpacity: 1, weight: 3, }).addTo(_recMap); } else { _recMarker.setLatLng(latlng); } _recMap.setView(latlng); } function _updateRecStats() { const dist = document.getElementById('rec-dist'); const pts = document.getElementById('rec-pts'); if (dist) dist.textContent = _distanceKm.toFixed(2); if (pts) pts.textContent = _track.length; } function _updateRecTimer() { const el = document.getElementById('rec-time'); if (!el) return; const secs = Math.floor((Date.now() - _startTime) / 1000); const mm = String(Math.floor(secs / 60)).padStart(2, '0'); const ss = String(secs % 60).padStart(2, '0'); el.textContent = `${mm}:${ss}`; } function _stopRecording() { if (_watchId !== null) { navigator.geolocation.clearWatch(_watchId); _watchId = null; } if (_timerInt) { clearInterval(_timerInt); _timerInt = null; } _recording = false; _paused = false; if (_track.length < 2) { UI.toast.warning('Zu wenige GPS-Punkte — bitte etwas länger gehen.'); _resetRecUI(); return; } const dauer = Math.floor((Date.now() - _startTime) / 1000 / 60); _showSaveForm(_track, _distanceKm, dauer); } function _resetRecUI() { document.getElementById('rec-idle').style.display = ''; document.getElementById('rec-active').style.display = 'none'; document.getElementById('rec-stats').style.display = 'none'; if (_recPolyline) { _recPolyline.remove(); _recPolyline = null; } if (_recMarker) { _recMarker.remove(); _recMarker = null; } _track = []; _distanceKm = 0; } // ---------------------------------------------------------- // Speicher-Formular nach Aufzeichnung // ---------------------------------------------------------- function _showSaveForm(track, distKm, dauMin) { const schwOpts = SCHWIERIGKEIT .map(s => ``) .join(''); const ugOpts = Object.entries(UNTERGRUND) .map(([v, l]) => ``) .join(''); const body = `🎉 ${track.length} GPS-Punkte · ${distKm.toFixed(2)} km · ca. ${dauMin} min
`; const footer = ` `; UI.modal.open({ title: '🥾 Route benennen', body, footer }); document.getElementById('rs-discard')?.addEventListener('click', () => { UI.modal.close(); _resetRecUI(); }); document.getElementById('route-save-form')?.addEventListener('submit', async e => { e.preventDefault(); const btn = document.querySelector('[form="route-save-form"][type="submit"]') || e.target.querySelector('[type="submit"]'); const fd = UI.formData(e.target); await UI.asyncButton(btn, async () => { const payload = { name: fd.name?.trim(), beschreibung: fd.beschreibung || null, gps_track: track, distanz_km: Math.round(distKm * 100) / 100, dauer_min: dauMin, schwierigkeit: fd.schwierigkeit || 'leicht', untergrund: fd.untergrund || null, schatten: 'schatten' in fd, leine_empfohlen: 'leine_empfohlen' in fd, }; const saved = await API.routes.create(payload); // Für die Liste: start_lat/lon aus Track ergänzen saved.start_lat = track[0].lat; saved.start_lon = track[0].lon; _data.unshift(saved); UI.modal.close(); _resetRecUI(); _renderList(); _renderPolylines(); _switchTab('entdecken'); UI.toast.success(`Route „${saved.name}" gespeichert! 🎉`); }); }); } return { init, refresh, onDogChange }; })();