MapLibre-Port Runde 3a: MapGLMarkers in map.js verdrahtet (flag-gated)

- _loadOsmLayers/_loadAll/_addPlaces/_addPoison/_addBreeders/_applyVisibility/_deleteUserPoi
  engine-neutral (GL: POI-DATEN statt Marker, MapGLMarkers.setLayer/setVisible)
- Standort-Dot, Place-Picker-Temp-Marker, Such-Marker als maplibregl.Marker
- engine-spez. Aufrufe über Facade (_mapFlyTo/_mapSetView/_mapResize)
- Wetter + GPS-Recording für GL vorerst gegated (Port in 3b/3c)
- Flag 'by_map_gl'/?mapgl=1, default AUS
This commit is contained in:
rene 2026-06-05 10:06:54 +02:00
parent 11922c1d22
commit 542106e77b
7 changed files with 295 additions and 70 deletions

View file

@ -1 +1 @@
1179
1180

View file

@ -86,14 +86,14 @@
<title>Ban Yaro</title>
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
<script src="/js/boot-early.js?v=1179"></script>
<script src="/js/boot-early.js?v=1180"></script>
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
<link rel="stylesheet" href="/css/design-system.css?v=1179">
<link rel="stylesheet" href="/css/layout.css?v=1179">
<link rel="stylesheet" href="/css/components.css?v=1179">
<link rel="stylesheet" href="/css/utilities.css?v=1179">
<link rel="stylesheet" href="/css/lists.css?v=1179">
<link rel="stylesheet" href="/css/design-system.css?v=1180">
<link rel="stylesheet" href="/css/layout.css?v=1180">
<link rel="stylesheet" href="/css/components.css?v=1180">
<link rel="stylesheet" href="/css/utilities.css?v=1180">
<link rel="stylesheet" href="/css/lists.css?v=1180">
</head>
<body>
@ -617,11 +617,11 @@
<div id="modal-container"></div>
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
<script src="/js/api.js?v=1179"></script>
<script src="/js/ui.js?v=1179"></script>
<script src="/js/app.js?v=1179"></script>
<script src="/js/worlds.js?v=1179"></script>
<script src="/js/offline-indicator.js?v=1179"></script>
<script src="/js/api.js?v=1180"></script>
<script src="/js/ui.js?v=1180"></script>
<script src="/js/app.js?v=1180"></script>
<script src="/js/worlds.js?v=1180"></script>
<script src="/js/offline-indicator.js?v=1180"></script>
<!-- Feature-Seiten werden lazy geladen -->
@ -631,7 +631,7 @@
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
<script src="/js/boot.js?v=1179"></script>
<script src="/js/boot.js?v=1180"></script>
</body>

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '1179'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '1180'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
window.APP_VERSION = APP_VERSION;

View file

@ -11,6 +11,7 @@
var _dangerRadiusM = 100;
var _popupHTML = null; // (props, key) -> htmlString
var _popupWire = null; // (props, key, closeFn) -> void
var _onClick = null; // (props, key) -> true = Klick behandelt, Popup unterdrücken
var _activePopup = null;
var _dangerKeys = [];
@ -132,6 +133,7 @@
if (!e.features || !e.features.length) return;
var f = e.features[0];
var props = f.properties || {};
if (_onClick && _onClick(props, key) === true) return; // Klick anderweitig behandelt
if (_activePopup) { _activePopup.remove(); _activePopup = null; }
var html = _popupHTML ? _popupHTML(props, key) : ('<b>' + (props.name || key) + '</b>');
if (!html) return;
@ -166,6 +168,7 @@
_dangerRadiusM = opts.dangerRadiusM || 100;
_popupHTML = opts.popupHTML || null;
_popupWire = opts.popupWire || null;
_onClick = opts.onClick || null;
_addCategoryLayers();
return _buildIcons();
},

View file

@ -172,7 +172,7 @@ window.Page_map = (() => {
API.getLocation().then(pos => {
_userPos = pos;
if (_frankfurtTimer) { clearTimeout(_frankfurtTimer); _frankfurtTimer = null; }
_map?.flyTo([pos.lat, pos.lon], 14, { duration: 1.2 });
_mapFlyTo(pos.lat, pos.lon, 14, { duration: 1.2 });
_weatherLoaded = true;
_loadWeather(pos.lat, pos.lon);
}).catch(() => {
@ -187,8 +187,8 @@ window.Page_map = (() => {
function refresh() {
// Leaflet kennt die Container-Größe nach Seitenwechsel nicht — neu berechnen
setTimeout(() => { _map?.invalidateSize(); _scheduleOsmLoad(); }, 150);
setTimeout(() => _map?.invalidateSize(), 600);
setTimeout(() => { _mapResize(); _scheduleOsmLoad(); }, 150);
setTimeout(() => _mapResize(), 600);
_loadAll();
}
function onDogChange() {}
@ -357,7 +357,7 @@ window.Page_map = (() => {
document.getElementById('map-locate-btn').addEventListener('click', () => {
_sdEl?.classList.remove('open');
if (_userPos) {
_map?.setView([_userPos.lat, _userPos.lon], 16);
_mapSetView(_userPos.lat, _userPos.lon, 16);
} else {
UI.toast.error('Standort noch nicht verfügbar.');
}
@ -425,6 +425,7 @@ window.Page_map = (() => {
let _tempDebounce = null;
async function _toggleRadar() {
if (_engineGL) { UI.toast.info('Regenradar im neuen Karten-Modus in Kürze.'); return; }
if (!App.hasPro(_appState?.user)) {
UI.toast.info('Regenradar ist ein Pro-Feature.');
return;
@ -445,6 +446,7 @@ window.Page_map = (() => {
}
async function _toggleTemp() {
if (_engineGL) { UI.toast.info('Temperatur-Layer im neuen Karten-Modus in Kürze.'); return; }
if (!App.hasPro(_appState?.user)) {
UI.toast.info('Temperatur-Layer ist ein Pro-Feature.');
return;
@ -690,13 +692,14 @@ window.Page_map = (() => {
const seq = (srcs) => srcs.reduce((p, src) => p.then(() => new Promise((res, rej) => {
if ((src.includes('maplibre-gl.js') && window.maplibregl) ||
(src.includes('pmtiles.js') && window.pmtiles) ||
(src.includes('map-gl-style') && window.MapGLStyle)) return res();
(src.includes('map-gl-style') && window.MapGLStyle) ||
(src.includes('map-gl-markers') && window.MapGLMarkers)) return res();
const s = document.createElement('script');
s.src = src + v; s.onload = res; s.onerror = rej;
document.head.appendChild(s);
})), Promise.resolve());
return seq(['/js/vendor/maplibre-gl.js', '/js/vendor/pmtiles.js', '/js/map-gl-style.js']).then(() => {
if (!(window.maplibregl && window.pmtiles && window.MapGLStyle)) throw new Error('MapLibre nicht geladen');
return seq(['/js/vendor/maplibre-gl.js', '/js/vendor/pmtiles.js', '/js/map-gl-style.js', '/js/map-gl-markers.js']).then(() => {
if (!(window.maplibregl && window.pmtiles && window.MapGLStyle && window.MapGLMarkers)) throw new Error('MapLibre nicht geladen');
if (!_pmtilesProtoReg) {
const proto = new pmtiles.Protocol();
maplibregl.addProtocol('pmtiles', proto.tile);
@ -783,23 +786,148 @@ window.Page_map = (() => {
if (!_map || !_engineGL) return;
_glLayersReady = false;
_map.setStyle(MapGLStyle.build({ dark: _isDarkMode() }));
// setStyle entfernt eigene Sources/Layer → nach Style-Load neu anlegen.
_map.once('styledata', () => { _initPoiLayersGL(); _scheduleOsmLoad(); });
// setStyle entfernt eigene Sources/Layer → nach Style-Load neu anlegen + Daten neu setzen.
_map.once('styledata', () => {
_initPoiLayersGL();
Object.keys(TYPEN).forEach(_glPushLayer);
_scheduleOsmLoad();
});
}
// GL-Datenmodell: POI-DATEN (nicht Marker) pro Kategorie. own = eigene Orte/Giftköder/
// Züchter (aus _loadAll), osm = Scan-Ergebnisse. Beim Setzen werden beide gemerged.
let _glOsm = {};
let _glOwn = {};
function _glPushLayer(key) {
if (!_engineGL || !window.MapGLMarkers) return;
MapGLMarkers.setLayer(key, (_glOwn[key] || []).concat(_glOsm[key] || []));
}
function _iconNameOf(t) {
const m = /#([a-z0-9-]+)"/.exec(t && t.icon || '');
return m ? m[1] : null;
}
// POI-Sources/Layer in MapLibre anlegen — wird in Build-Runde 2 gefüllt.
function _initPoiLayersGL() {
if (!_map || !_engineGL || _glLayersReady) return;
if (!_map || !_engineGL || !window.MapGLMarkers || _glLayersReady) return;
_glLayersReady = true;
// (Build-Runde 2: GeoJSON-Sources + Cluster/Symbol-Layer + Icons)
const types = {};
Object.keys(TYPEN).forEach(k => {
types[k] = { color: TYPEN[k].color, iconName: _iconNameOf(TYPEN[k]), danger: DANGER_RADIUS[k] !== undefined };
});
MapGLMarkers.init(_map, {
types,
dangerKeys: Object.keys(DANGER_RADIUS),
dangerRadiusM: 100,
onClick: (props) => {
if (props._kind === 'poison_alarm') { App.navigate('poison'); return true; }
if (props._kind === 'place') {
UI.toast.info(`${props.name || ''}${props.adresse ? ' · ' + props.adresse : ''}`.trim() || 'Eigener Ort');
return true;
}
return false;
},
popupHTML: (props, key) => _buildPoiPopupHTML(props, key),
popupWire: (props, key, close) => _wirePoiPopup(props, key, close),
});
}
// Popup-HTML für GL (spiegelt _showMarkerPopup; Züchter separat).
function _buildPoiPopupHTML(props, layerKey) {
const t = TYPEN[layerKey] || {};
if (props._kind === 'breeder') {
const rasse = props.rasse_text ? `<div style="font-size:12px;color:#666;margin-bottom:4px">${UI.escape(props.rasse_text)}</div>` : '';
const stadt = props.stadt ? `<div style="font-size:12px;color:#888;margin-bottom:8px">${UI.escape(props.stadt)}</div>` : '';
return `<div style="min-width:170px;max-width:240px">
<div style="font-weight:600;margin-bottom:6px">${t.icon || ''} ${UI.escape(props.zwingername || '')}</div>
${rasse}${stadt}<button class="btn btn-primary btn-sm" id="breeder-profile-btn">Profil ansehen</button></div>`;
}
const label = props.name || t.label || '';
const isOwn = props.source === 'user' && (props.own === true || props.own === 'true' || props.own === 1);
const isUser = props.source === 'user';
const DOG_TYPES = ['restaurant', 'hotel', 'shop', 'tierarzt', 'hundesalon'];
const dogBtn = (props.source === 'osm' && DOG_TYPES.includes(layerKey))
? `<div style="margin-bottom:8px"><div style="font-size:11px;color:#666;margin-bottom:4px">Hund willkommen?</div>
<div style="display:flex;gap:6px">
<button class="btn btn-secondary btn-sm" id="mp-dogyes" style="flex:1" title="Hund willkommen"><svg class="ph-icon" aria-hidden="true" style="width:15px;height:15px"><use href="/icons/phosphor.svg#thumbs-up"></use></svg></button>
<button class="btn btn-secondary btn-sm" id="mp-dogno" style="flex:1" title="Hund nicht willkommen"><svg class="ph-icon" aria-hidden="true" style="width:15px;height:15px"><use href="/icons/phosphor.svg#thumbs-down"></use></svg></button>
</div></div>` : '';
const actionBtn = isOwn
? `<button class="btn btn-danger btn-sm" id="mp-action">Löschen</button>`
: `<button class="btn btn-secondary btn-sm" id="mp-action">Als ungültig melden</button>`;
const openHours = props.opening_hours ? `<div style="font-size:11px;color:#555;margin-bottom:4px"><svg class="ph-icon" aria-hidden="true" style="width:12px;height:12px"><use href="/icons/phosphor.svg#clock"></use></svg> ${UI.escape(String(props.opening_hours))}</div>` : '';
const phone = props.phone ? `<div style="font-size:11px;margin-bottom:4px"><a href="tel:${props.phone}" style="color:var(--c-primary);text-decoration:none">${UI.escape(String(props.phone))}</a></div>` : '';
const website = props.website ? `<div style="font-size:11px;margin-bottom:6px"><a href="${props.website}" target="_blank" rel="noopener" style="color:var(--c-primary);text-decoration:none"><svg class="ph-icon" aria-hidden="true" style="width:12px;height:12px"><use href="/icons/phosphor.svg#arrow-square-out"></use></svg> Website</a></div>` : '';
return `<div style="min-width:170px;max-width:240px">
<div style="font-weight:600;margin-bottom:6px">${t.icon || ''} ${UI.escape(String(label))}</div>
${props.notiz ? `<div style="font-size:12px;color:#666;margin-bottom:6px">${UI.escape(String(props.notiz))}</div>` : ''}
${openHours}${phone}${website}
<div style="font-size:11px;color:#999;margin-bottom:10px">
${isUser ? `<svg class="ph-icon" aria-hidden="true" style="width:11px;height:11px"><use href="/icons/phosphor.svg#push-pin"></use></svg> Community-Pin${props.username ? ' · <b>' + UI.escape(String(props.username)) + '</b>' : ''}`
: '<svg class="ph-icon" aria-hidden="true" style="width:11px;height:11px"><use href="/icons/phosphor.svg#map-trifold"></use></svg> OpenStreetMap'}
</div>${dogBtn}${actionBtn}</div>`;
}
function _wirePoiPopup(props, layerKey, close) {
if (props._kind === 'breeder') {
document.getElementById('breeder-profile-btn')?.addEventListener('click', () => {
close(); App.navigate('breeder', true, { zwingername: props.zwingername });
});
return;
}
const isOwn = props.source === 'user' && (props.own === true || props.own === 'true' || props.own === 1);
document.getElementById('mp-action')?.addEventListener('click', () => {
close();
if (isOwn) _deleteUserPoi(props.user_poi_id, null, layerKey);
else _showReportDialog({ source: props.source, id: props.id, user_poi_id: props.user_poi_id, lat: props.lat, lon: props.lon });
});
const sendDog = async (welcome) => {
const yes = document.getElementById('mp-dogyes'), no = document.getElementById('mp-dogno');
if (yes) yes.disabled = true; if (no) no.disabled = true;
try {
const r = await API.post('/osm-contrib/dog-friendly', {
osm_id: props.id, osm_type: 'node', poi_type: layerKey, lat: props.lat, lon: props.lon, welcome,
});
UI.toast.success((welcome ? 'Hund willkommen' : 'Hund nicht willkommen') + (r.submitted ? ' — eingetragen 🐾' : ' — wird übertragen 🐾'));
close();
} catch (e) {
UI.toast.error(e?.message || 'Konnte nicht eintragen.');
if (yes) yes.disabled = false; if (no) no.disabled = false;
}
};
document.getElementById('mp-dogyes')?.addEventListener('click', () => sendDog(true));
document.getElementById('mp-dogno')?.addEventListener('click', () => sendDog(false));
}
// ----------------------------------------------------------
// Standort-Tracking — pulsierender blauer Punkt
// ----------------------------------------------------------
function _startLocationTracking() {
if (!navigator.geolocation || !_map || !window.L) return;
if (!navigator.geolocation || !_map) return;
if (_engineGL) {
_watchId = navigator.geolocation.watchPosition(
pos => {
const { latitude: lat, longitude: lon } = pos.coords;
_userPos = { lat, lon };
if (!_weatherLoaded) { _weatherLoaded = true; _loadWeather(lat, lon); }
if (_locationMarker) {
_locationMarker.setLngLat([lon, lat]);
} else {
const elx = document.createElement('div');
elx.className = 'loc-icon';
elx.innerHTML = '<div class="loc-dot"></div><div class="loc-ring"></div>';
_locationMarker = new maplibregl.Marker({ element: elx, anchor: 'center' })
.setLngLat([lon, lat]).addTo(_map);
}
},
() => {},
{ enableHighAccuracy: true, maximumAge: 5000, timeout: 15000 }
);
return;
}
if (!window.L) return;
const icon = L.divIcon({
className: 'loc-icon',
html: '<div class="loc-dot"></div><div class="loc-ring"></div>',
@ -1110,54 +1238,69 @@ window.Page_map = (() => {
_overpassTimer = setTimeout(_loadOsmLayers, 600);
}
// OSM-Marker-Zählung (ohne eigene Orte), engine-neutral.
function _osmCountOf(k) {
if (_engineGL) return (_glOsm[k] || []).length;
return (_layers[k] || []).filter(m => !m._ownPlace).length;
}
function _osmTotalCount() {
if (_engineGL) return Object.values(_glOsm).reduce((a, arr) => a + (arr ? arr.length : 0), 0);
return Object.values(_layers).flat().filter(m => !m._ownPlace).length;
}
// OSM-Marker eines Layers leeren (engine-neutral, eigene Orte bleiben).
function _clearOsmLayer(k) {
if (_engineGL) { _glOsm[k] = []; _glPushLayer(k); return; }
_layers[k].filter(m => !m._ownPlace).forEach(m => m._dangerCircle?.remove());
_clusterGroups[k]?.clearLayers();
_layers[k] = _layers[k].filter(m => m._ownPlace);
}
async function _loadOsmLayers() {
if (!_map || !window.L || _overpassActive) return;
const zoom = _map.getZoom();
if (!_map || _overpassActive) return;
if (!_engineGL && !window.L) return;
const zoom = _mapGetZoom();
// Unter Zoom 10: alles ausblenden
if (zoom < 10) {
Object.keys(OSM_LAYER_MAP).forEach(k => {
_layers[k].filter(m => !m._ownPlace).forEach(m => m._dangerCircle?.remove());
_clusterGroups[k]?.clearLayers();
_layers[k] = _layers[k].filter(m => m._ownPlace);
});
Object.keys(OSM_LAYER_MAP).forEach(_clearOsmLayer);
_setOsmStatus('');
return;
}
// Zoom 1013: normale OSM-Layer ausblenden, EARLY_LAYERS behalten/laden
if (zoom < 14) {
Object.keys(OSM_LAYER_MAP).filter(k => !EARLY_LAYERS.has(k)).forEach(k => {
_layers[k].filter(m => !m._ownPlace).forEach(m => m._dangerCircle?.remove());
_clusterGroups[k]?.clearLayers();
_layers[k] = _layers[k].filter(m => m._ownPlace);
});
Object.keys(OSM_LAYER_MAP).filter(k => !EARLY_LAYERS.has(k)).forEach(_clearOsmLayer);
}
_overpassActive = true;
_map.invalidateSize();
const b = _map.getBounds().pad(0.15);
const bbox = { south: b.getSouth(), west: b.getWest(), north: b.getNorth(), east: b.getEast() };
_mapResize();
let bbox;
if (_engineGL) {
const p = _mapPaddedBounds(0.15);
bbox = { south: p.south, west: p.west, north: p.north, east: p.east };
} else {
const b = _map.getBounds().pad(0.15);
bbox = { south: b.getSouth(), west: b.getWest(), north: b.getNorth(), east: b.getEast() };
}
// Welche Layer bei diesem Zoom geladen werden
const activeLayers = zoom >= 14
? Object.entries(OSM_LAYER_MAP)
: Object.entries(OSM_LAYER_MAP).filter(([k]) => EARLY_LAYERS.has(k));
// OSM-Marker eines Layers ersetzen, eigene Orte behalten
// OSM-Marker eines Layers ersetzen, eigene Orte behalten (engine-neutral)
function _replaceOsmMarkers(layerKey, pois) {
if (_engineGL) { _glOsm[layerKey] = pois || []; _glPushLayer(layerKey); return; }
const cluster = _getCluster(layerKey);
// Alte OSM-Marker entfernen
const oldOsm = _layers[layerKey].filter(m => !m._ownPlace);
oldOsm.forEach(m => m._dangerCircle?.remove());
cluster.removeLayers(oldOsm);
_layers[layerKey] = _layers[layerKey].filter(m => m._ownPlace);
// Neue Marker erstellen und in Cluster packen
const t = TYPEN[layerKey];
const newMarkers = pois.map(poi => _createOsmMarker(poi, layerKey, t));
cluster.addLayers(newMarkers);
_layers[layerKey].push(...newMarkers);
// Sicherstellen dass der Cluster auf der Karte ist (kann durch vorherigen Toggle fehlen)
if (_visible[layerKey] !== false && _map && !_map.hasLayer(cluster)) {
cluster.addTo(_map);
}
@ -1186,19 +1329,19 @@ 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 = _osmCountOf(layerKey);
if (pois.length !== osmCount) _replaceOsmMarkers(layerKey, pois);
_done++;
const pct = Math.round(20 + _done / _total * 80);
const total = Object.values(_layers).flat().filter(m => !m._ownPlace).length;
const total = _osmTotalCount();
_setOsmStatus(pct < 100 ? `Scanne…` : `${total} Marker`, pct);
return pois.length;
} catch {
_done++;
const pct = Math.round(20 + _done / _total * 80);
const total = Object.values(_layers).flat().filter(m => !m._ownPlace).length;
const total = _osmTotalCount();
_setOsmStatus(pct < 100 ? `Scanne…` : `${total} Marker`, pct);
return (_layers[layerKey] || []).filter(m => !m._ownPlace).length;
return _osmCountOf(layerKey);
}
});
try {
@ -1207,7 +1350,7 @@ window.Page_map = (() => {
_overpassActive = false;
}
const totalLoaded = Object.values(_layers).flat().filter(m => !m._ownPlace).length;
const totalLoaded = _osmTotalCount();
const allHidden = Object.keys(OSM_LAYER_MAP).every(k => _visible[k] === false);
if (totalLoaded > 0 && allHidden) {
_setOsmStatus('Layer deaktiviert — Liste antippen', 100);
@ -1408,9 +1551,16 @@ window.Page_map = (() => {
function _confirmPlacement(latlng) {
_tempMarker?.remove();
_tempMarker = L.circleMarker([latlng.lat, latlng.lng], {
radius: 10, color: '#F59E0B', fillColor: '#F59E0B', fillOpacity: 0.6,
}).addTo(_map);
if (_engineGL) {
const dot = document.createElement('div');
dot.style.cssText = 'width:20px;height:20px;border-radius:50%;background:#F59E0B;opacity:.7;border:2px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,.4)';
_tempMarker = new maplibregl.Marker({ element: dot, anchor: 'center' })
.setLngLat([latlng.lng, latlng.lat]).addTo(_map);
} else {
_tempMarker = L.circleMarker([latlng.lat, latlng.lng], {
radius: 10, color: '#F59E0B', fillColor: '#F59E0B', fillOpacity: 0.6,
}).addTo(_map);
}
let _selectedTypes = new Set(['giftkoeder']);
@ -1547,9 +1697,16 @@ window.Page_map = (() => {
method: 'DELETE', credentials: 'include',
});
if (!res.ok) throw new Error();
_clusterGroups[layerKey]?.removeLayer(marker);
marker._dangerCircle?.remove();
_layers[layerKey] = _layers[layerKey].filter(m => m !== marker);
if (_engineGL) {
const drop = arr => (arr || []).filter(p => String(p.user_poi_id) !== String(poiId));
_glOsm[layerKey] = drop(_glOsm[layerKey]);
_glOwn[layerKey] = drop(_glOwn[layerKey]);
_glPushLayer(layerKey);
} else {
_clusterGroups[layerKey]?.removeLayer(marker);
marker._dangerCircle?.remove();
_layers[layerKey] = _layers[layerKey].filter(m => m !== marker);
}
UI.toast.success('Marker gelöscht.');
} catch { UI.toast.error('Fehler beim Löschen.'); }
}
@ -1560,16 +1717,18 @@ window.Page_map = (() => {
async function _loadAll() {
// Falls Overpass-Job steckengeblieben: zurücksetzen
_overpassActive = false;
// Cluster-Gruppen leeren (OSM-Marker)
Object.values(_clusterGroups).forEach(cg => cg.clearLayers());
// Eigene-Orte-Marker direkt von Karte entfernen
Object.values(_layers).flat().filter(m => m._ownPlace).forEach(m => {
m._dangerCircle?.remove();
m.remove();
});
// Giftköder-Kreise
(_layers.poison || []).forEach(m => m._dangerCircle?.remove());
Object.keys(_layers).forEach(k => { _layers[k] = []; });
if (_engineGL) {
_glOwn = {}; // eigene-Orte-Daten leeren
Object.keys(TYPEN).forEach(_glPushLayer); // OSM-Scan-Daten bleiben
} else {
Object.values(_clusterGroups).forEach(cg => cg.clearLayers());
Object.values(_layers).flat().filter(m => m._ownPlace).forEach(m => {
m._dangerCircle?.remove();
m.remove();
});
(_layers.poison || []).forEach(m => m._dangerCircle?.remove());
Object.keys(_layers).forEach(k => { _layers[k] = []; });
}
const [places, poisonList, breederList] = await Promise.allSettled([
API.places.list(),
@ -1616,6 +1775,19 @@ window.Page_map = (() => {
}
function _addPlaces(places) {
if (_engineGL) {
const touched = new Set();
places.forEach(place => {
if (!TYPEN[place.typ]) return;
(_glOwn[place.typ] = _glOwn[place.typ] || []).push({
lat: place.lat, lon: place.lon, name: place.name, adresse: place.adresse,
_kind: 'place', source: 'place',
});
touched.add(place.typ);
});
touched.forEach(_glPushLayer);
return;
}
if (!_map || !window.L) return;
places.forEach(place => {
const t = TYPEN[place.typ];
@ -1629,6 +1801,16 @@ window.Page_map = (() => {
}
function _addPoison(items) {
if (_engineGL) {
items.forEach(p => {
(_glOwn.poison = _glOwn.poison || []).push({
lat: p.lat, lon: p.lon, name: 'Giftk\u00f6der-Alarm', beschreibung: p.beschreibung,
_kind: 'poison_alarm', source: 'poison',
});
});
_glPushLayer('poison');
return;
}
if (!_map || !window.L) return;
items.forEach(p => {
const tooltip = `Giftk\u00f6der-Alarm${p.beschreibung ? ': ' + p.beschreibung : ''}`;
@ -1647,6 +1829,17 @@ window.Page_map = (() => {
}
function _addBreeders(breeders) {
if (_engineGL) {
breeders.forEach(b => {
if (b.location_lat == null || b.location_lng == null) return;
(_glOwn.zuechter = _glOwn.zuechter || []).push({
lat: b.location_lat, lon: b.location_lng, _kind: 'breeder', source: 'breeder',
zwingername: b.zwingername, rasse_text: b.rasse_text, stadt: b.stadt,
});
});
_glPushLayer('zuechter');
return;
}
if (!_map || !window.L) return;
const t = TYPEN.zuechter;
const cluster = _getCluster('zuechter');
@ -1721,6 +1914,18 @@ window.Page_map = (() => {
function _applyVisibility(layer) {
// poison-Toggle steuert auch giftkoeder-Community-Pins mit
const keys = layer === 'poison' ? ['poison', 'giftkoeder'] : [layer];
if (_engineGL) {
keys.forEach(k => {
const on = _visible[layer];
_visible[k] = on;
if (window.MapGLMarkers) MapGLMarkers.setVisible(k, on);
if (k === 'poison' && _map && _map.getLayer && _map.getLayer('danger-fill')) {
const vis = on ? 'visible' : 'none';
['danger-fill', 'danger-line'].forEach(id => { if (_map.getLayer(id)) _map.setLayoutProperty(id, 'visibility', vis); });
}
});
return;
}
keys.forEach(k => {
const on = _visible[layer];
_visible[k] = on;
@ -1902,6 +2107,7 @@ window.Page_map = (() => {
// GPS-Aufzeichnung
// ----------------------------------------------------------
function _toggleRecording() {
if (_engineGL && !_recActive) { UI.toast.info('GPS-Aufzeichnung im neuen Karten-Modus in Kürze.'); return; }
if (!_recActive) _startRecording();
else _stopRecording();
}
@ -1945,6 +2151,7 @@ window.Page_map = (() => {
}
async function _startRecording(resume) {
if (_engineGL) { if (!resume) UI.toast.info('GPS-Aufzeichnung im neuen Karten-Modus in Kürze.'); return; }
if (!_appState.user) {
UI.toast.warning('Bitte zuerst anmelden.');
App.navigate('settings');
@ -2359,8 +2566,23 @@ window.Page_map = (() => {
}
function _flyToResult(r) {
if (!_map || !window.L) return;
if (!_map) return;
_searchMarker?.remove();
if (_engineGL) {
_mapFlyTo(r.lat, r.lon, 15, { duration: 1.0 });
const pin = document.createElement('div');
pin.style.cssText = 'width:30px;height:30px;border-radius:50% 50% 50% 0;transform:rotate(-45deg);background:#C4843A;border:2px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,.4)';
_searchMarker = new maplibregl.Marker({ element: pin, anchor: 'bottom' })
.setLngLat([r.lon, r.lat]).addTo(_map);
const popup = new maplibregl.Popup({ maxWidth: '240px' }).setLngLat([r.lon, r.lat])
.setHTML(`<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>`)
.addTo(_map);
setTimeout(() => { document.getElementById('search-marker-close')?.addEventListener('click', () => { _clearSearch(); popup.remove(); }); }, 50);
return;
}
if (!window.L) return;
_map.flyTo([r.lat, r.lon], 15, { duration: 1.0 });
_searchMarker = L.marker([r.lat, r.lon], {
icon: L.divIcon({

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<script src="/js/landing-init.js?v=1179"></script>
<script src="/js/landing-init.js?v=1180"></script>
<title>Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz</title>
<meta name="description" content="Ban Yaro: Die kostenlose All-in-One Hunde-App für DACH. Tagebuch, Giftköder-Alarm, Training mit KI, Forum, Wurfbörse, Stammbaum, Inzucht-Check — DSGVO-konform, offline-fähig, ohne App Store.">
<meta name="keywords" content="Hunde App, Hunde Community, Wurfbörse, Züchter, Welpen kaufen, Stammbaum Hund, Inzuchtkoeffizient, Hundezucht, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundetraining App, Hunde Forum, Hunde KI, Hundefilm Datenbank, Welpen Marktplatz">

View file

@ -4,7 +4,7 @@
============================================================ */
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
const VER = '1179';
const VER = '1180';
const CACHE_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten