Feature: Rundweg-Vorschläge via OpenRouteService — 2/4/6 km, 3 Varianten, Navigation+Speichern — SW by-v478, APP_VER 455
This commit is contained in:
parent
b09a569689
commit
369eae5e5a
5 changed files with 396 additions and 19 deletions
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '454'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '455'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
|
||||
const App = (() => {
|
||||
|
||||
|
|
|
|||
|
|
@ -40,9 +40,15 @@ window.Page_routes = (() => {
|
|||
let _recPolyline = null, _recLocMarker = null;
|
||||
let _recWakeLock = null, _recInactTimer = null, _recDimmed = false;
|
||||
|
||||
// 'mine' | 'discover'
|
||||
// 'mine' | 'discover' | 'suggest'
|
||||
let _browseMode = 'mine';
|
||||
|
||||
// Vorschläge-Tab state
|
||||
let _suggestKm = 4; // gewählte Distanz: 2, 4 oder 6
|
||||
let _suggestSeed = 0; // Variante: 0, 1, 2
|
||||
let _suggestResult = null; // letzte API-Antwort
|
||||
let _suggestMap = null; // Leaflet-Instanz der Vorschau-Karte
|
||||
|
||||
// Ansichts-Modus: 'list' | 'map'
|
||||
let _viewMode = 'list';
|
||||
let _searchMap = null; // L.map Instanz der Suchkarte
|
||||
|
|
@ -121,9 +127,10 @@ window.Page_routes = (() => {
|
|||
<div class="rk-mode-toggle" id="rk-mode-toggle">
|
||||
<button class="rk-mode-btn${_browseMode==='mine'?' active':''}" id="rk-mode-mine">${UI.icon('user')} Meine Routen</button>
|
||||
<button class="rk-mode-btn${_browseMode==='discover'?' active':''}" id="rk-mode-discover">${UI.icon('map-pin')} Entdecken</button>
|
||||
<button class="rk-mode-btn${_browseMode==='suggest'?' active':''}" id="rk-mode-suggest">${UI.icon('sparkle')} Vorschläge</button>
|
||||
</div>
|
||||
<!-- Zeile 2: Suche + View-Toggle (gleiche Höhe wie Aktions-Buttons) -->
|
||||
<div style="display:flex;gap:8px;align-items:stretch;margin-bottom:var(--space-3)">
|
||||
<div id="rk-search-row" style="display:flex;gap:8px;align-items:stretch;margin-bottom:var(--space-3)">
|
||||
<div style="position:relative;flex:1;min-width:0">
|
||||
<svg class="ph-icon" aria-hidden="true"
|
||||
style="position:absolute;left:12px;top:50%;transform:translateY(-50%);
|
||||
|
|
@ -253,6 +260,7 @@ window.Page_routes = (() => {
|
|||
// Mode toggle
|
||||
document.getElementById('rk-mode-mine').addEventListener('click', () => _setBrowseMode('mine'));
|
||||
document.getElementById('rk-mode-discover').addEventListener('click', () => _setBrowseMode('discover'));
|
||||
document.getElementById('rk-mode-suggest').addEventListener('click', () => _setBrowseMode('suggest'));
|
||||
}
|
||||
|
||||
function _syncRecBtn() {
|
||||
|
|
@ -278,26 +286,306 @@ window.Page_routes = (() => {
|
|||
_browseMode = mode;
|
||||
document.getElementById('rk-mode-mine')?.classList.toggle('active', mode === 'mine');
|
||||
document.getElementById('rk-mode-discover')?.classList.toggle('active', mode === 'discover');
|
||||
document.getElementById('rk-mode-suggest')?.classList.toggle('active', mode === 'suggest');
|
||||
|
||||
const recBtn = document.getElementById('rk-rec-btn');
|
||||
const impWrap = document.getElementById('rk-imp-wrap');
|
||||
const mineGrp = document.getElementById('rk-mine-group');
|
||||
const recBtn = document.getElementById('rk-rec-btn');
|
||||
const impWrap = document.getElementById('rk-imp-wrap');
|
||||
const mineGrp = document.getElementById('rk-mine-group');
|
||||
const nearbyGrp = document.getElementById('rk-nearby-group');
|
||||
const searchRow = document.getElementById('rk-search-row'); // Zeile 2: Suche + View-Toggle
|
||||
const filterBtn = document.getElementById('rk-filter-btn');
|
||||
const actRow = filterBtn?.parentElement; // Zeile 3: Aktions-Buttons
|
||||
|
||||
if (mode === 'discover') {
|
||||
if (recBtn) recBtn.style.display = 'none';
|
||||
if (impWrap) impWrap.style.display = 'none';
|
||||
if (mineGrp) mineGrp.style.display = 'none';
|
||||
if (nearbyGrp && _userPos) nearbyGrp.style.display = '';
|
||||
} else {
|
||||
if (recBtn) recBtn.style.display = '';
|
||||
if (impWrap) impWrap.style.display = '';
|
||||
if (_appState.user && mineGrp) mineGrp.style.display = '';
|
||||
if (mode === 'suggest') {
|
||||
if (recBtn) recBtn.style.display = 'none';
|
||||
if (impWrap) impWrap.style.display = 'none';
|
||||
if (mineGrp) mineGrp.style.display = 'none';
|
||||
if (nearbyGrp) nearbyGrp.style.display = 'none';
|
||||
if (searchRow) searchRow.style.display = 'none';
|
||||
if (actRow) actRow.style.display = 'none';
|
||||
const filterPanel = document.getElementById('rk-filter-panel');
|
||||
if (filterPanel) filterPanel.style.display = 'none';
|
||||
_renderSuggestTab();
|
||||
} else {
|
||||
if (searchRow) searchRow.style.display = '';
|
||||
if (actRow) actRow.style.display = '';
|
||||
if (mode === 'discover') {
|
||||
if (recBtn) recBtn.style.display = 'none';
|
||||
if (impWrap) impWrap.style.display = 'none';
|
||||
if (mineGrp) mineGrp.style.display = 'none';
|
||||
if (nearbyGrp && _userPos) nearbyGrp.style.display = '';
|
||||
} else {
|
||||
if (recBtn) recBtn.style.display = '';
|
||||
if (impWrap) impWrap.style.display = '';
|
||||
if (_appState.user && mineGrp) mineGrp.style.display = '';
|
||||
if (nearbyGrp) nearbyGrp.style.display = 'none';
|
||||
}
|
||||
_onlyMine = false;
|
||||
document.querySelectorAll('#rk-mine-group .rk-chip').forEach(c => c.classList.remove('active'));
|
||||
_applyFilter();
|
||||
}
|
||||
_onlyMine = false;
|
||||
document.querySelectorAll('#rk-mine-group .rk-chip').forEach(c => c.classList.remove('active'));
|
||||
_applyFilter();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Vorschläge-Tab
|
||||
// ----------------------------------------------------------
|
||||
function _renderSuggestTab() {
|
||||
const grid = document.getElementById('rk-grid');
|
||||
if (!grid) return;
|
||||
|
||||
// Leaflet-Karte aus vorherigem Besuch aufräumen
|
||||
if (_suggestMap) { _suggestMap.remove(); _suggestMap = null; }
|
||||
|
||||
// Styles einmalig injizieren
|
||||
if (!document.getElementById('rk-suggest-styles')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'rk-suggest-styles';
|
||||
style.textContent = `
|
||||
.rks-km-chip {
|
||||
flex:1;padding:14px 8px;border-radius:var(--radius-lg);
|
||||
border:2px solid var(--c-border-light);background:var(--c-surface);
|
||||
color:var(--c-text);font-size:1.1rem;font-weight:700;cursor:pointer;
|
||||
transition:border-color .15s,background .15s,color .15s;text-align:center;
|
||||
}
|
||||
.rks-km-chip.active {
|
||||
border-color:var(--c-primary);background:var(--c-primary);color:#fff;
|
||||
}
|
||||
.rks-var-btn {
|
||||
flex:1;padding:8px 4px;border-radius:8px;font-size:0.8rem;font-weight:600;
|
||||
border:1.5px solid var(--c-border-light);background:var(--c-surface);
|
||||
color:var(--c-text-secondary);cursor:pointer;
|
||||
transition:border-color .15s,background .15s,color .15s;
|
||||
}
|
||||
.rks-var-btn.active {
|
||||
border-color:var(--c-primary);color:var(--c-primary);background:rgba(var(--c-primary-rgb,99,102,241),0.08);
|
||||
}
|
||||
#rks-map { border-radius:var(--radius-lg);overflow:hidden; }
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
grid.innerHTML = `
|
||||
<div style="padding:var(--space-4) var(--space-2);display:flex;flex-direction:column;gap:var(--space-5)">
|
||||
|
||||
<!-- Distanz-Auswahl -->
|
||||
<div>
|
||||
<div style="font-size:0.75rem;font-weight:600;color:var(--c-text-secondary);
|
||||
text-transform:uppercase;letter-spacing:.06em;margin-bottom:var(--space-3)">
|
||||
Gewünschte Distanz
|
||||
</div>
|
||||
<div style="display:flex;gap:var(--space-3)" id="rks-km-row">
|
||||
<button class="rks-km-chip${_suggestKm===2?' active':''}" data-km="2">2 km</button>
|
||||
<button class="rks-km-chip${_suggestKm===4?' active':''}" data-km="4">4 km</button>
|
||||
<button class="rks-km-chip${_suggestKm===6?' active':''}" data-km="6">6 km</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Varianten-Auswahl -->
|
||||
<div>
|
||||
<div style="font-size:0.75rem;font-weight:600;color:var(--c-text-secondary);
|
||||
text-transform:uppercase;letter-spacing:.06em;margin-bottom:var(--space-3)">
|
||||
Variante
|
||||
</div>
|
||||
<div style="display:flex;gap:var(--space-2)" id="rks-var-row">
|
||||
<button class="rks-var-btn${_suggestSeed===0?' active':''}" data-seed="0">Variante 1</button>
|
||||
<button class="rks-var-btn${_suggestSeed===1?' active':''}" data-seed="1">Variante 2</button>
|
||||
<button class="rks-var-btn${_suggestSeed===2?' active':''}" data-seed="2">Variante 3</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Berechnen-Button -->
|
||||
<button id="rks-calc-btn" style="${_btnStyle(true)}">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#path"></use></svg>
|
||||
Route berechnen
|
||||
</button>
|
||||
|
||||
<!-- Ergebnis-Bereich (initial leer) -->
|
||||
<div id="rks-result"></div>
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Distanz-Chips
|
||||
grid.querySelector('#rks-km-row').addEventListener('click', e => {
|
||||
const btn = e.target.closest('.rks-km-chip');
|
||||
if (!btn) return;
|
||||
_suggestKm = parseInt(btn.dataset.km, 10);
|
||||
grid.querySelectorAll('.rks-km-chip').forEach(b => b.classList.toggle('active', b === btn));
|
||||
});
|
||||
|
||||
// Varianten-Buttons
|
||||
grid.querySelector('#rks-var-row').addEventListener('click', e => {
|
||||
const btn = e.target.closest('.rks-var-btn');
|
||||
if (!btn) return;
|
||||
_suggestSeed = parseInt(btn.dataset.seed, 10);
|
||||
grid.querySelectorAll('.rks-var-btn').forEach(b => b.classList.toggle('active', b === btn));
|
||||
});
|
||||
|
||||
// Berechnen
|
||||
grid.querySelector('#rks-calc-btn').addEventListener('click', _calcSuggestRoute);
|
||||
}
|
||||
|
||||
async function _calcSuggestRoute() {
|
||||
// Standort prüfen
|
||||
if (!_userPos) {
|
||||
try { _userPos = await API.getLocation(); } catch {
|
||||
const res = document.getElementById('rks-result');
|
||||
if (res) res.innerHTML = `
|
||||
<div style="padding:var(--space-5);text-align:center;color:var(--c-text-secondary);
|
||||
border:1.5px solid var(--c-border-light);border-radius:var(--radius-lg);
|
||||
background:var(--c-surface)">
|
||||
<svg class="ph-icon" aria-hidden="true" style="width:32px;height:32px;color:var(--c-text-muted);margin-bottom:var(--space-3)">
|
||||
<use href="/icons/phosphor.svg#map-pin-slash"></use>
|
||||
</svg>
|
||||
<p style="margin:0;font-size:0.9rem">
|
||||
Standort wird benötigt. Bitte erlaube den Zugriff in den Browser-Einstellungen.
|
||||
</p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Alten Karteninhalt aufräumen
|
||||
if (_suggestMap) { _suggestMap.remove(); _suggestMap = null; }
|
||||
|
||||
// Spinner anzeigen
|
||||
const res = document.getElementById('rks-result');
|
||||
if (!res) return;
|
||||
res.innerHTML = `
|
||||
<div style="display:flex;flex-direction:column;align-items:center;gap:var(--space-4);
|
||||
padding:var(--space-6);color:var(--c-text-secondary)">
|
||||
<div style="width:32px;height:32px;border:3px solid var(--c-border);
|
||||
border-top-color:var(--c-primary);border-radius:50%;
|
||||
animation:spin 0.8s linear infinite"></div>
|
||||
<span style="font-size:0.9rem">Berechne Rundweg…</span>
|
||||
</div>
|
||||
<style>@keyframes spin{to{transform:rotate(360deg)}}</style>
|
||||
`;
|
||||
|
||||
const calcBtn = document.getElementById('rks-calc-btn');
|
||||
if (calcBtn) calcBtn.disabled = true;
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = await API.post('/routes/suggest', {
|
||||
lat: _userPos.lat,
|
||||
lon: _userPos.lon,
|
||||
distance_km: _suggestKm,
|
||||
seed: _suggestSeed,
|
||||
});
|
||||
_suggestResult = result;
|
||||
} catch (err) {
|
||||
if (res) res.innerHTML = `
|
||||
<div style="padding:var(--space-4);border-radius:var(--radius-lg);
|
||||
background:rgba(220,38,38,0.08);border:1px solid rgba(220,38,38,0.25);
|
||||
color:#f87171;font-size:0.9rem">
|
||||
${UI.icon('warning')} ${UI.escape(err.message || 'Fehler beim Berechnen des Rundwegs.')}
|
||||
</div>`;
|
||||
if (calcBtn) calcBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
if (calcBtn) calcBtn.disabled = false;
|
||||
|
||||
// Ergebnis rendern
|
||||
const distStr = result.distanz_km ? result.distanz_km.toFixed(2) + ' km' : '–';
|
||||
const durStr = result.dauer_min
|
||||
? (result.dauer_min < 60 ? result.dauer_min + ' min' : Math.floor(result.dauer_min/60) + 'h ' + (result.dauer_min%60||'') + 'min').trim()
|
||||
: '–';
|
||||
const diffLabel = { leicht: 'Leicht', mittel: 'Mittel', anspruchsvoll: 'Schwer' }[result.schwierigkeit] || '';
|
||||
|
||||
if (!res) return;
|
||||
res.innerHTML = `
|
||||
<!-- Karte -->
|
||||
<div id="rks-map" style="height:250px;background:var(--c-surface);margin-bottom:var(--space-3)"></div>
|
||||
|
||||
<!-- Info-Zeile -->
|
||||
<div style="display:flex;gap:var(--space-3);align-items:center;flex-wrap:wrap;margin-bottom:var(--space-4)">
|
||||
<span style="${_pillStyle('rgba(107,114,128,0.10)','#9ca3af','rgba(107,114,128,0.30)')}">
|
||||
${UI.icon('map-trifold')} ${UI.escape(distStr)}
|
||||
</span>
|
||||
<span style="${_pillStyle('rgba(107,114,128,0.10)','#9ca3af','rgba(107,114,128,0.30)')}">
|
||||
${UI.icon('timer')} ${UI.escape(durStr)}
|
||||
</span>
|
||||
${diffLabel ? `<span style="${_pillStyle(
|
||||
{leicht:'rgba(22,163,74,0.10)',mittel:'rgba(234,179,8,0.10)',anspruchsvoll:'rgba(220,38,38,0.10)'}[result.schwierigkeit]||'rgba(107,114,128,0.10)',
|
||||
{leicht:'#4ade80',mittel:'#facc15',anspruchsvoll:'#f87171'}[result.schwierigkeit]||'#9ca3af',
|
||||
{leicht:'rgba(22,163,74,0.30)',mittel:'rgba(234,179,8,0.30)',anspruchsvoll:'rgba(220,38,38,0.30)'}[result.schwierigkeit]||'rgba(107,114,128,0.30)')}">${UI.escape(diffLabel)}</span>` : ''}
|
||||
<span style="font-size:0.85rem;color:var(--c-text-secondary);flex:1;min-width:0;
|
||||
overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${UI.escape(result.name || '')}</span>
|
||||
</div>
|
||||
|
||||
<!-- Aktions-Buttons -->
|
||||
<div style="display:flex;gap:var(--space-3)">
|
||||
<button id="rks-nav-btn" style="${_btnStyle(false)}flex:1">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#navigation-arrow"></use></svg>
|
||||
Navigation starten
|
||||
</button>
|
||||
<button id="rks-save-btn" style="${_btnStyle(true)}flex:1">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#floppy-disk"></use></svg>
|
||||
Route speichern
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Leaflet-Karte mit dem berechneten Track
|
||||
const _initMap = () => {
|
||||
const mapEl = document.getElementById('rks-map');
|
||||
if (!mapEl || !window.L) return;
|
||||
if (_suggestMap) { _suggestMap.remove(); _suggestMap = null; }
|
||||
|
||||
const track = result.gps_track || [];
|
||||
if (track.length < 2) { mapEl.innerHTML = '<div style="height:100%;display:flex;align-items:center;justify-content:center;color:var(--c-text-muted)">Kein Track vorhanden</div>'; return; }
|
||||
|
||||
const lls = track.map(p => [p.lat, p.lon]);
|
||||
_suggestMap = L.map(mapEl, { zoomControl: false, attributionControl: false,
|
||||
dragging: true, touchZoom: true, scrollWheelZoom: false });
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(_suggestMap);
|
||||
const poly = L.polyline(lls, { color: '#C4843A', weight: 4, opacity: 0.9 }).addTo(_suggestMap);
|
||||
L.circleMarker(lls[0], { radius:7, color:'#22C55E', fillColor:'#22C55E', fillOpacity:1, weight:2 }).addTo(_suggestMap);
|
||||
L.circleMarker(lls.at(-1), { radius:7, color:'#EF4444', fillColor:'#EF4444', fillOpacity:1, weight:2 }).addTo(_suggestMap);
|
||||
_addRouteArrows(_suggestMap, track, '#3b82f6');
|
||||
_suggestMap.fitBounds(poly.getBounds(), { padding: [16, 16] });
|
||||
setTimeout(() => _suggestMap?.invalidateSize(), 120);
|
||||
};
|
||||
|
||||
if (window.L) {
|
||||
_initMap();
|
||||
} else {
|
||||
let tries = 0;
|
||||
const poll = setInterval(() => {
|
||||
if (window.L || ++tries > 40) { clearInterval(poll); if (window.L) _initMap(); }
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Navigation starten
|
||||
document.getElementById('rks-nav-btn')?.addEventListener('click', () => {
|
||||
if (!_suggestResult) return;
|
||||
const route = {
|
||||
id: 'suggest-' + Date.now(),
|
||||
name: _suggestResult.name,
|
||||
gps_track: _suggestResult.gps_track,
|
||||
distanz_km: _suggestResult.distanz_km,
|
||||
};
|
||||
_openNavOverlay(route);
|
||||
});
|
||||
|
||||
// Route speichern
|
||||
document.getElementById('rks-save-btn')?.addEventListener('click', async () => {
|
||||
const btn = document.getElementById('rks-save-btn');
|
||||
if (!btn || !_suggestResult) return;
|
||||
await UI.asyncButton(btn, async () => {
|
||||
await API.post('/routes', {
|
||||
name: _suggestResult.name,
|
||||
gps_track: _suggestResult.gps_track,
|
||||
distanz_km: _suggestResult.distanz_km,
|
||||
dauer_min: _suggestResult.dauer_min,
|
||||
schwierigkeit: _suggestResult.schwierigkeit,
|
||||
});
|
||||
UI.toast.success('Route gespeichert!');
|
||||
await _loadData();
|
||||
_setBrowseMode('mine');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function _loadDataNearby() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue