/* ============================================================
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 = `
${Object.entries(TYPEN).map(([k, t]) => `
`).join('')}
`;
// 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;
// Explizite Pixel-Höhe setzen, damit Leaflet Kacheln lädt.
// height:100% auf einem flex:1-Child ohne feste Parent-Höhe = 0px.
const layout = el.closest('.map-full-layout');
if (layout) {
const top = layout.getBoundingClientRect().top;
layout.style.height = (window.innerHeight - top) + 'px';
window.addEventListener('resize', () => {
const t = layout.getBoundingClientRect().top;
layout.style.height = (window.innerHeight - t) + 'px';
_map?.invalidateSize();
});
}
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);
setTimeout(() => _map.invalidateSize(), 100);
}
// ----------------------------------------------------------
// 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: `${t.icon}
`,
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 };
})();