banyaro/backend/static/js/pages/map.js
rene b9df636535 Sprint 6: Karte / Orte / Routen mit GPS-Aufzeichnung
- backend/routes/places.py: CRUD für hundefreundliche Orte (6 Typen)
- backend/routes/routen.py: CRUD für Gassi-Routen mit GPS-Track (JSON)
- main.py: beide Router eingehängt (/api/places, /api/routes)
- api.js: places + routes erweitert (list, update, delete)
- pages/places.js: Karte + Liste, Typ-Filter, Ort anlegen/bearbeiten
- pages/routes.js: Routen entdecken + GPS-Aufzeichnung mit Stoppuhr
- pages/map.js: zentrale Übersichtskarte (Orte + Giftköder, Layer-Toggle)
- components.css: Styles für alle drei neuen Seiten
- sw.js: by-v19 → by-v20
2026-04-14 06:03:37 +02:00

230 lines
7.6 KiB
JavaScript

/* ============================================================
BAN YARO — Zentrale Karte
Alle Layer auf einer Karte: Orte + Giftköder
============================================================ */
window.Page_map = (() => {
let _container = null;
let _appState = null;
let _map = null;
let _leafletLoaded = false;
let _userPos = null;
// Layer-Marker
let _layers = {
restaurant: [],
freilauf: [],
shop: [],
kotbeutel: [],
tierarzt: [],
hundeschule: [],
poison: [],
};
// Layer-Sichtbarkeit
let _visible = {
restaurant: true,
freilauf: true,
shop: true,
kotbeutel: true,
tierarzt: true,
hundeschule: true,
poison: true,
};
const TYPEN = {
restaurant: { icon: '🍽️', label: 'Restaurant', color: '#F97316' },
freilauf: { icon: '🐕', label: 'Freilauf', color: '#22C55E' },
shop: { icon: '🛒', label: 'Shop', color: '#3B82F6' },
kotbeutel: { icon: '🧻', label: 'Kotbeutel', color: '#6B7280' },
tierarzt: { icon: '🩺', label: 'Tierarzt', color: '#EF4444' },
hundeschule: { icon: '🎓', label: 'Hundeschule', color: '#8B5CF6' },
poison: { icon: '⚠️', label: 'Giftköder', color: '#DC2626' },
};
// ----------------------------------------------------------
// INIT
// ----------------------------------------------------------
async function init(container, appState) {
_container = container;
_appState = appState;
_render();
try { _userPos = await API.getLocation(); } catch {}
await _loadLeaflet();
_initMap();
_loadAll();
}
function refresh() { _loadAll(); }
function onDogChange() {}
// ----------------------------------------------------------
// RENDER
// ----------------------------------------------------------
function _render() {
_container.innerHTML = `
<div class="map-full-layout">
<!-- Layer-Legende -->
<div class="map-legend" id="map-legend">
${Object.entries(TYPEN).map(([k, t]) => `
<button class="map-legend-btn active" data-layer="${k}"
style="--layer-color:${t.color}">
${t.icon} ${t.label}
</button>
`).join('')}
</div>
<!-- Karte -->
<div id="central-map" class="map-full"></div>
<!-- GPS-Button -->
<button class="map-locate-btn" id="map-locate-btn" title="Meinen Standort">📍</button>
</div>
`;
// Layer-Toggle
document.getElementById('map-legend').addEventListener('click', e => {
const btn = e.target.closest('.map-legend-btn');
if (!btn) return;
const layer = btn.dataset.layer;
_visible[layer] = !_visible[layer];
btn.classList.toggle('active', _visible[layer]);
_applyVisibility(layer);
});
// GPS-Locate
document.getElementById('map-locate-btn').addEventListener('click', async () => {
try {
_userPos = await API.getLocation({ enableHighAccuracy: true });
_map?.setView([_userPos.lat, _userPos.lon], 14);
} catch { UI.toast.error('Standort konnte nicht ermittelt werden.'); }
});
}
// ----------------------------------------------------------
// Leaflet laden
// ----------------------------------------------------------
async function _loadLeaflet() {
if (_leafletLoaded || window.L) { _leafletLoaded = true; return; }
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/css/leaflet.css';
document.head.appendChild(link);
await new Promise(resolve => {
const s = document.createElement('script');
s.src = '/js/leaflet.js';
s.onload = resolve;
document.head.appendChild(s);
});
_leafletLoaded = true;
}
// ----------------------------------------------------------
// Karte initialisieren
// ----------------------------------------------------------
function _initMap() {
const el = document.getElementById('central-map');
if (!el || !window.L || _map) return;
const center = _userPos ? [_userPos.lat, _userPos.lon] : [51.1657, 10.4515];
const zoom = _userPos ? 13 : 6;
_map = L.map('central-map', { zoomControl: true, attributionControl: false })
.setView(center, zoom);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 })
.addTo(_map);
}
// ----------------------------------------------------------
// Alle Layer laden
// ----------------------------------------------------------
async function _loadAll() {
// Alles zurücksetzen
Object.values(_layers).flat().forEach(m => m.remove?.());
_layers = { restaurant: [], freilauf: [], shop: [], kotbeutel: [], tierarzt: [], hundeschule: [], poison: [] };
// Parallel laden
const [places, poisonList] = await Promise.allSettled([
API.places.list(),
_userPos
? API.poison.listNearby(_userPos.lat, _userPos.lon, 10000)
: Promise.resolve([]),
]);
if (places.status === 'fulfilled') _addPlaces(places.value);
if (poisonList.status === 'fulfilled') _addPoison(poisonList.value);
}
// ----------------------------------------------------------
// Orte-Marker
// ----------------------------------------------------------
function _addPlaces(places) {
if (!_map || !window.L) return;
places.forEach(place => {
const t = TYPEN[place.typ];
if (!t) return;
const m = _createMarker(place.lat, place.lon, t, place.name, () => _showPlacePopup(place));
_layers[place.typ]?.push(m);
if (!_visible[place.typ]) m.setOpacity(0);
});
}
function _showPlacePopup(place) {
const t = TYPEN[place.typ] || { icon: '📍', label: place.typ };
UI.toast.info(`${t.icon} ${place.name}${place.adresse ? ' · ' + place.adresse : ''}`);
}
// ----------------------------------------------------------
// Giftköder-Marker
// ----------------------------------------------------------
function _addPoison(items) {
if (!_map || !window.L) return;
items.forEach(p => {
const t = TYPEN.poison;
const m = _createMarker(p.lat, p.lon, t, `Giftköder-Alarm${p.beschreibung ? ': ' + p.beschreibung : ''}`, () => {
App.navigate('poison');
});
_layers.poison.push(m);
if (!_visible.poison) m.setOpacity(0);
});
}
// ----------------------------------------------------------
// Marker-Hilfsfunktion
// ----------------------------------------------------------
function _createMarker(lat, lon, t, tooltip, onClick) {
const icon = L.divIcon({
className: '',
html: `<div style="
background:${t.color};color:#fff;font-size:15px;
width:32px;height:32px;border-radius:50%;
display:flex;align-items:center;justify-content:center;
box-shadow:0 2px 5px rgba(0,0,0,0.35);
border:2px solid rgba(255,255,255,0.7)
">${t.icon}</div>`,
iconSize: [32, 32],
iconAnchor: [16, 16],
});
return L.marker([lat, lon], { icon })
.addTo(_map)
.bindTooltip(tooltip, { direction: 'top', offset: [0, -16] })
.on('click', onClick);
}
// ----------------------------------------------------------
// Layer ein/ausblenden
// ----------------------------------------------------------
function _applyVisibility(layer) {
(_layers[layer] || []).forEach(m => {
// Leaflet hat keine native hide — Opacity-Trick
if (m.setOpacity) m.setOpacity(_visible[layer] ? 1 : 0);
});
}
return { init, refresh, onDogChange };
})();