Karten-Ausbau (OSM), Forum-Erweiterung, UI-Komponenten, Refactor Tagebuch/Gassi (DRY), Landing/SEO — APP_VER 1155
This commit is contained in:
parent
2d907f6370
commit
10e39ed135
18 changed files with 871 additions and 405 deletions
|
|
@ -59,6 +59,7 @@ window.Page_map = (() => {
|
|||
treffpunkt: [],
|
||||
community: [],
|
||||
zuechter: [],
|
||||
hotel: [],
|
||||
};
|
||||
|
||||
const VISIBLE_KEY = 'by_map_visible_v1';
|
||||
|
|
@ -130,6 +131,10 @@ window.Page_map = (() => {
|
|||
interactive: false,
|
||||
};
|
||||
|
||||
// Orts-Suche
|
||||
let _searchTimer = null;
|
||||
let _searchMarker = null;
|
||||
|
||||
let _overpassTimer = null;
|
||||
let _overpassActive = false;
|
||||
let _ringClosing = false;
|
||||
|
|
@ -210,13 +215,50 @@ window.Page_map = (() => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="map-fabs">
|
||||
<button class="map-fab map-fab--pin" id="map-pin-btn" title="Marker setzen"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#push-pin"></use></svg></button>
|
||||
${App.hasPro(_appState?.user) ? `
|
||||
<button class="map-fab" id="map-radar-btn" title="Regenradar ein-/ausblenden"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#cloud-rain"></use></svg></button>
|
||||
<button class="map-fab" id="map-temp-btn" title="Temperatur ein-/ausblenden"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#thermometer"></use></svg></button>
|
||||
` : ''}
|
||||
<button class="map-fab" id="map-locate-btn" title="Meinen Standort"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg></button>
|
||||
<!-- Orts-Suche Panel (von oben einschiebend, geschlossen per default) -->
|
||||
<div class="map-search-wrap" id="map-search-wrap">
|
||||
<div class="map-search-row">
|
||||
<svg class="ph-icon" aria-hidden="true" style="width:16px;height:16px;flex-shrink:0;color:#888"><use href="/icons/phosphor.svg#magnifying-glass"></use></svg>
|
||||
<input type="search" id="map-search-input" class="map-search-input"
|
||||
placeholder="Ort oder Adresse…" autocomplete="off" autocorrect="off" spellcheck="false">
|
||||
<button class="map-search-clear" id="map-search-clear" aria-label="Suche schließen">
|
||||
<svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px"><use href="/icons/phosphor.svg#x"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="map-search-results" id="map-search-results" style="display:none"></div>
|
||||
</div>
|
||||
|
||||
<!-- Speed Dial -->
|
||||
<div class="map-speed-dial" id="map-speed-dial">
|
||||
<div class="map-sd-items">
|
||||
<!-- DOM-Reihenfolge = Aufklappreihenfolge von unten nach oben -->
|
||||
<div class="map-sd-item">
|
||||
<span class="map-sd-label">Mein Standort</span>
|
||||
<button class="map-sd-btn" id="map-locate-btn" title="Mein Standort"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg></button>
|
||||
</div>
|
||||
<div class="map-sd-item">
|
||||
<span class="map-sd-label">Ort suchen</span>
|
||||
<button class="map-sd-btn" id="map-search-btn" title="Ort suchen"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#magnifying-glass"></use></svg></button>
|
||||
</div>
|
||||
<div class="map-sd-item">
|
||||
<span class="map-sd-label">Marker setzen</span>
|
||||
<button class="map-sd-btn map-fab--pin" id="map-pin-btn" title="Marker setzen"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#push-pin"></use></svg></button>
|
||||
</div>
|
||||
${App.hasPro(_appState?.user) ? `
|
||||
<div class="map-sd-item">
|
||||
<span class="map-sd-label">Regenradar</span>
|
||||
<button class="map-sd-btn" id="map-radar-btn" title="Regenradar"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#cloud-rain"></use></svg></button>
|
||||
</div>
|
||||
<div class="map-sd-item">
|
||||
<span class="map-sd-label">Temperatur</span>
|
||||
<button class="map-sd-btn" id="map-temp-btn" title="Temperatur"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#thermometer"></use></svg></button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<button class="map-fab map-sd-trigger" id="map-sd-trigger" title="Karten-Aktionen">
|
||||
<svg class="ph-icon map-sd-icon-open" aria-hidden="true"><use href="/icons/phosphor.svg#dots-three-vertical"></use></svg>
|
||||
<svg class="ph-icon map-sd-icon-close" aria-hidden="true"><use href="/icons/phosphor.svg#x"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="map-statusbar" id="map-statusbar">
|
||||
|
|
@ -289,7 +331,19 @@ window.Page_map = (() => {
|
|||
_saveVisible();
|
||||
});
|
||||
|
||||
// Speed Dial
|
||||
const _sdEl = document.getElementById('map-speed-dial');
|
||||
document.getElementById('map-sd-trigger')?.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
_sdEl?.classList.toggle('open');
|
||||
});
|
||||
// Klick auf Karte / außerhalb schließt Speed Dial
|
||||
document.getElementById('central-map')?.addEventListener('pointerdown', () => {
|
||||
_sdEl?.classList.remove('open');
|
||||
});
|
||||
|
||||
document.getElementById('map-locate-btn').addEventListener('click', () => {
|
||||
_sdEl?.classList.remove('open');
|
||||
if (_userPos) {
|
||||
_map?.setView([_userPos.lat, _userPos.lon], 16);
|
||||
} else {
|
||||
|
|
@ -297,9 +351,54 @@ window.Page_map = (() => {
|
|||
}
|
||||
});
|
||||
|
||||
document.getElementById('map-pin-btn').addEventListener('click', _togglePlacementMode);
|
||||
document.getElementById('map-radar-btn')?.addEventListener('click', _toggleRadar);
|
||||
document.getElementById('map-temp-btn')?.addEventListener('click', _toggleTemp);
|
||||
document.getElementById('map-pin-btn').addEventListener('click', () => {
|
||||
_sdEl?.classList.remove('open');
|
||||
_togglePlacementMode();
|
||||
});
|
||||
document.getElementById('map-radar-btn')?.addEventListener('click', () => {
|
||||
_sdEl?.classList.remove('open');
|
||||
_toggleRadar();
|
||||
});
|
||||
document.getElementById('map-temp-btn')?.addEventListener('click', () => {
|
||||
_sdEl?.classList.remove('open');
|
||||
_toggleTemp();
|
||||
});
|
||||
|
||||
// Suche — FAB öffnet Panel
|
||||
document.getElementById('map-search-btn')?.addEventListener('click', () => {
|
||||
document.getElementById('map-speed-dial')?.classList.remove('open');
|
||||
const wrap = document.getElementById('map-search-wrap');
|
||||
const isOpen = wrap?.classList.contains('active');
|
||||
if (isOpen) {
|
||||
_clearSearch();
|
||||
} else {
|
||||
wrap?.classList.add('active');
|
||||
setTimeout(() => document.getElementById('map-search-input')?.focus(), 60);
|
||||
document.getElementById('map-search-btn')?.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
const searchInput = document.getElementById('map-search-input');
|
||||
const searchResults = document.getElementById('map-search-results');
|
||||
|
||||
searchInput?.addEventListener('input', () => {
|
||||
const q = searchInput.value.trim();
|
||||
clearTimeout(_searchTimer);
|
||||
if (q.length < 2) { searchResults.style.display = 'none'; return; }
|
||||
_searchTimer = setTimeout(() => _runSearch(q), 400);
|
||||
});
|
||||
|
||||
searchInput?.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') _clearSearch();
|
||||
});
|
||||
|
||||
document.getElementById('map-search-clear')?.addEventListener('click', _clearSearch);
|
||||
|
||||
// Klick auf Karte schließt Ergebnisse (aber behält Marker)
|
||||
document.getElementById('central-map')?.addEventListener('pointerdown', () => {
|
||||
searchResults.style.display = 'none';
|
||||
searchInput?.blur();
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -907,7 +1006,7 @@ window.Page_map = (() => {
|
|||
const params = new URLSearchParams({ type: osmType, ...bbox });
|
||||
try {
|
||||
const pois = await fetch(`/api/osm/pois?${params}`).then(r => r.json());
|
||||
const osmCount = _layers[layerKey].filter(m => !m._ownPlace).length;
|
||||
const osmCount = (_layers[layerKey] || []).filter(m => !m._ownPlace).length;
|
||||
if (pois.length !== osmCount) _replaceOsmMarkers(layerKey, pois);
|
||||
_done++;
|
||||
const pct = Math.round(20 + _done / _total * 80);
|
||||
|
|
@ -919,11 +1018,14 @@ window.Page_map = (() => {
|
|||
const pct = Math.round(20 + _done / _total * 80);
|
||||
const total = Object.values(_layers).flat().filter(m => !m._ownPlace).length;
|
||||
_setOsmStatus(pct < 100 ? `Scanne…` : `${total} Marker`, pct);
|
||||
return _layers[layerKey].filter(m => !m._ownPlace).length;
|
||||
return (_layers[layerKey] || []).filter(m => !m._ownPlace).length;
|
||||
}
|
||||
});
|
||||
await Promise.all(freshTasks);
|
||||
_overpassActive = false;
|
||||
try {
|
||||
await Promise.all(freshTasks);
|
||||
} finally {
|
||||
_overpassActive = false;
|
||||
}
|
||||
|
||||
const totalLoaded = Object.values(_layers).flat().filter(m => !m._ownPlace).length;
|
||||
const allHidden = Object.keys(OSM_LAYER_MAP).every(k => _visible[k] === false);
|
||||
|
|
@ -931,10 +1033,13 @@ window.Page_map = (() => {
|
|||
_setOsmStatus('Layer deaktiviert — Liste antippen', 100);
|
||||
}
|
||||
|
||||
// Wenn 0 OSM-Marker: Hintergrund-Fetch läuft noch — max 3× automatisch nachfragen
|
||||
if (totalLoaded === 0 && zoom >= 14 && _autoRetryCount < 3) {
|
||||
// Wenn 0 OSM-Marker: Hintergrund-Overpass-Fetch läuft noch — bis zu 8× nachfragen
|
||||
// Overpass für alle Layer sequential: bis zu ~4min → Retries müssen das abdecken
|
||||
if (totalLoaded === 0 && zoom >= 14 && _autoRetryCount < 8) {
|
||||
_autoRetryCount++;
|
||||
const delay = _autoRetryCount * 30000; // 30s, 60s, 90s
|
||||
// 10s, 20s, 35s, 50s, 70s, 90s, 120s, 150s
|
||||
const delays = [10000, 20000, 35000, 50000, 70000, 90000, 120000, 150000];
|
||||
const delay = delays[_autoRetryCount - 1] || 120000;
|
||||
_setOsmStatus(`Neue Umgebung – Daten werden geladen…`);
|
||||
setTimeout(() => { if (!_overpassActive) _scheduleOsmLoad(); }, delay);
|
||||
}
|
||||
|
|
@ -1944,6 +2049,92 @@ window.Page_map = (() => {
|
|||
} catch { /* still */ }
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Orts-Suche (Nominatim-Proxy)
|
||||
// ----------------------------------------------------------
|
||||
async function _runSearch(q) {
|
||||
const resultsEl = document.getElementById('map-search-results');
|
||||
if (!resultsEl) return;
|
||||
resultsEl.innerHTML = '<div class="map-search-loading">Suche…</div>';
|
||||
resultsEl.style.display = '';
|
||||
try {
|
||||
const data = await API.get(`/osm/geocode?q=${encodeURIComponent(q)}`);
|
||||
if (!data.length) {
|
||||
resultsEl.innerHTML = '<div class="map-search-empty">Keine Ergebnisse</div>';
|
||||
return;
|
||||
}
|
||||
resultsEl.innerHTML = data.map((r, i) =>
|
||||
`<div class="map-search-item" data-i="${i}">
|
||||
<div class="map-search-item-name">${UI.escape(r.name)}</div>
|
||||
${r.subtitle ? `<div class="map-search-item-sub">${UI.escape(r.subtitle)}</div>` : ''}
|
||||
</div>`
|
||||
).join('');
|
||||
resultsEl.querySelectorAll('.map-search-item').forEach(el => {
|
||||
el.addEventListener('pointerdown', e => {
|
||||
e.stopPropagation();
|
||||
const r = data[+el.dataset.i];
|
||||
_flyToResult(r);
|
||||
document.getElementById('map-search-input').value = r.name;
|
||||
document.getElementById('map-search-clear').style.display = '';
|
||||
resultsEl.style.display = 'none';
|
||||
});
|
||||
});
|
||||
} catch {
|
||||
resultsEl.innerHTML = '<div class="map-search-empty">Suche nicht verfügbar</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function _flyToResult(r) {
|
||||
if (!_map || !window.L) return;
|
||||
_searchMarker?.remove();
|
||||
_map.flyTo([r.lat, r.lon], 15, { duration: 1.0 });
|
||||
_searchMarker = L.marker([r.lat, r.lon], {
|
||||
icon: L.divIcon({
|
||||
className: '',
|
||||
html: `<div style="background:#C4843A;color:#fff;font-size:15px;
|
||||
width:32px;height:32px;border-radius:50% 50% 50% 0;transform:rotate(-45deg);
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
box-shadow:0 2px 8px rgba(0,0,0,0.4)">
|
||||
<span style="transform:rotate(45deg)">
|
||||
<svg style="width:16px;height:16px" viewBox="0 0 256 256" fill="currentColor">
|
||||
<path d="M128,16a96,96,0,1,0,96,96A96.11,96.11,0,0,0,128,16Zm0,48a32,32,0,1,1-32,32A32,32,0,0,1,128,64Zm0,144a80,80,0,0,1-56.37-23.37C74.18,170.06,98.65,160,128,160s53.82,10.06,56.37,24.63A80,80,0,0,1,128,208Z"/>
|
||||
</svg>
|
||||
</span></div>`,
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 32],
|
||||
}),
|
||||
zIndexOffset: 1000,
|
||||
})
|
||||
.addTo(_map)
|
||||
.bindPopup(`<div style="font-size:13px;font-weight:600">${UI.escape(r.name)}</div>
|
||||
${r.subtitle ? `<div style="font-size:11px;color:#888">${UI.escape(r.subtitle)}</div>` : ''}
|
||||
<button class="btn btn-secondary btn-sm" id="search-marker-close" style="margin-top:8px">
|
||||
Marker entfernen
|
||||
</button>`, { maxWidth: 240 })
|
||||
.openPopup();
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById('search-marker-close')?.addEventListener('click', () => {
|
||||
_clearSearch();
|
||||
_searchMarker?.closePopup();
|
||||
});
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function _clearSearch() {
|
||||
const input = document.getElementById('map-search-input');
|
||||
const results = document.getElementById('map-search-results');
|
||||
const wrap = document.getElementById('map-search-wrap');
|
||||
const btn = document.getElementById('map-search-btn');
|
||||
if (input) { input.value = ''; input.blur(); }
|
||||
if (results) results.style.display = 'none';
|
||||
wrap?.classList.remove('active');
|
||||
btn?.classList.remove('active');
|
||||
_searchMarker?.remove();
|
||||
_searchMarker = null;
|
||||
clearTimeout(_searchTimer);
|
||||
}
|
||||
|
||||
return { init, refresh, onDogChange, startRecording: _startRecording, stopRecording: _stopRecording, isRecording: () => _recActive };
|
||||
|
||||
})();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue