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:
parent
11922c1d22
commit
542106e77b
7 changed files with 295 additions and 70 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
1179
|
||||
1180
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 10–13: 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({
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue