diff --git a/VERSION b/VERSION
index d19618e..baa70bd 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1179
\ No newline at end of file
+1180
\ No newline at end of file
diff --git a/backend/static/index.html b/backend/static/index.html
index 78d55fd..e8f270c 100644
--- a/backend/static/index.html
+++ b/backend/static/index.html
@@ -86,14 +86,14 @@
Ban Yaro
-
+
-
-
-
-
-
+
+
+
+
+
@@ -617,11 +617,11 @@
-
-
-
-
-
+
+
+
+
+
@@ -631,7 +631,7 @@
-
+
diff --git a/backend/static/js/app.js b/backend/static/js/app.js
index 9c378f5..a736339 100644
--- a/backend/static/js/app.js
+++ b/backend/static/js/app.js
@@ -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;
diff --git a/backend/static/js/map-gl-markers.js b/backend/static/js/map-gl-markers.js
index abdf84a..defa7a8 100644
--- a/backend/static/js/map-gl-markers.js
+++ b/backend/static/js/map-gl-markers.js
@@ -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) : ('' + (props.name || key) + '');
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();
},
diff --git a/backend/static/js/pages/map.js b/backend/static/js/pages/map.js
index 88617fc..66c0b6b 100644
--- a/backend/static/js/pages/map.js
+++ b/backend/static/js/pages/map.js
@@ -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 ? `