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
This commit is contained in:
parent
956e34db88
commit
b9df636535
9 changed files with 1948 additions and 9 deletions
230
backend/static/js/pages/map.js
Normal file
230
backend/static/js/pages/map.js
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
/* ============================================================
|
||||
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 };
|
||||
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue