244 lines
8 KiB
JavaScript
244 lines
8 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;
|
|
|
|
// Map-Container braucht position:relative + kein Padding,
|
|
// damit .map-full-layout mit position:absolute;inset:0 korrekt füllt.
|
|
Object.assign(_container.style, {
|
|
padding: '0',
|
|
overflow: 'hidden',
|
|
position: 'relative',
|
|
gap: '0',
|
|
});
|
|
|
|
_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);
|
|
|
|
// invalidateSize nach kurzer Verzögerung, damit der Browser das Layout abgeschlossen hat
|
|
setTimeout(() => _map.invalidateSize(), 150);
|
|
window.addEventListener('resize', () => _map.invalidateSize());
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// 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 };
|
|
|
|
})();
|