Seitenkarten auf MapLibre GL (Facade) — Runde 1: Giftköder + Verlorene
- map-gl-mini.js: Leaflet-kompatible MapLibre-Facade (createMap/svgMarker/circleMarker/ featureGroup-Wrapper mit setView/fitBounds/invalidateSize/addTo/bindPopup/openPopup/on/remove) - ui.js: UI.map.create/svgMarker/leafletMarker branchen auf GL (by_map_gl, Staging-Default), + UI.map.circleMarker/featureGroup, loadMapLibreUI - poison.js/lost.js: window.L-Guards entfernt, L.circleMarker→UI.map.circleMarker
This commit is contained in:
parent
9c4b999331
commit
5844f1ef51
9 changed files with 239 additions and 25 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
1194
|
||||
1195
|
||||
|
|
@ -86,14 +86,14 @@
|
|||
<title>Ban Yaro</title>
|
||||
|
||||
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
|
||||
<script src="/js/boot-early.js?v=1194"></script>
|
||||
<script src="/js/boot-early.js?v=1195"></script>
|
||||
|
||||
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1194">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1194">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1194">
|
||||
<link rel="stylesheet" href="/css/utilities.css?v=1194">
|
||||
<link rel="stylesheet" href="/css/lists.css?v=1194">
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1195">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1195">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1195">
|
||||
<link rel="stylesheet" href="/css/utilities.css?v=1195">
|
||||
<link rel="stylesheet" href="/css/lists.css?v=1195">
|
||||
</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=1194"></script>
|
||||
<script src="/js/ui.js?v=1194"></script>
|
||||
<script src="/js/app.js?v=1194"></script>
|
||||
<script src="/js/worlds.js?v=1194"></script>
|
||||
<script src="/js/offline-indicator.js?v=1194"></script>
|
||||
<script src="/js/api.js?v=1195"></script>
|
||||
<script src="/js/ui.js?v=1195"></script>
|
||||
<script src="/js/app.js?v=1195"></script>
|
||||
<script src="/js/worlds.js?v=1195"></script>
|
||||
<script src="/js/offline-indicator.js?v=1195"></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=1194"></script>
|
||||
<script src="/js/boot.js?v=1195"></script>
|
||||
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '1194'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '1195'; // ← 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;
|
||||
|
|
|
|||
146
backend/static/js/map-gl-mini.js
Normal file
146
backend/static/js/map-gl-mini.js
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
// Leaflet-kompatible MapLibre-Facade für die SEITENKARTEN (Giftköder, Verlorene,
|
||||
// Events, Gassi, Routen). Liefert Wrapper, die die von den Seiten genutzte Leaflet-
|
||||
// API nachbilden (setView/fitBounds/invalidateSize/addTo/bindPopup/openPopup/on/remove),
|
||||
// sodass die Seiten fast unverändert auf demselben GL-Style (MapGLStyle) laufen.
|
||||
// Koordinaten nach außen [lat,lon] (Leaflet-Konvention), intern MapLibre [lng,lat].
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function _ll(latlon) { return [latlon[1], latlon[0]]; } // [lat,lon] → [lng,lat]
|
||||
|
||||
// ---- Map-Wrapper ----
|
||||
function _wrapMap(map) {
|
||||
return {
|
||||
_gl: map,
|
||||
_isGL: true,
|
||||
setView: function (latlon, zoom) { map.jumpTo({ center: _ll(latlon), zoom: zoom }); return this; },
|
||||
flyTo: function (latlon, zoom, opts) {
|
||||
map.flyTo({ center: _ll(latlon), zoom: zoom, duration: opts && opts.duration ? opts.duration * 1000 : 1000 });
|
||||
return this;
|
||||
},
|
||||
panTo: function (latlon) { map.panTo(_ll(latlon)); return this; },
|
||||
fitBounds: function (b, opts) {
|
||||
var bb = _toBounds(b);
|
||||
if (bb) {
|
||||
var pad = 40;
|
||||
if (opts && opts.padding) pad = Array.isArray(opts.padding) ? opts.padding[0] : opts.padding;
|
||||
map.fitBounds(bb, { padding: pad, maxZoom: opts && opts.maxZoom, duration: 0 });
|
||||
}
|
||||
return this;
|
||||
},
|
||||
invalidateSize: function () { map.resize(); return this; },
|
||||
removeLayer: function (layer) { if (layer && layer.remove) layer.remove(); return this; },
|
||||
addLayer: function (layer) { if (layer && layer.addTo) layer.addTo(this); return this; },
|
||||
hasLayer: function () { return true; },
|
||||
remove: function () { try { map.remove(); } catch (e) {} },
|
||||
on: function (ev, fn) { map.on(ev, fn); return this; },
|
||||
off: function (ev, fn) { map.off(ev, fn); return this; },
|
||||
getZoom: function () { return map.getZoom(); },
|
||||
getCenter: function () { var c = map.getCenter(); return { lat: c.lat, lng: c.lng }; },
|
||||
};
|
||||
}
|
||||
|
||||
// Bounds aus: Array von [lat,lon] | featureGroup-Wrapper (_coords) | Leaflet-Bounds.
|
||||
function _toBounds(b) {
|
||||
if (!b) return null;
|
||||
var coords = null;
|
||||
if (Array.isArray(b)) coords = b;
|
||||
else if (b._coords) coords = b._coords;
|
||||
else if (typeof b.getSouthWest === 'function') {
|
||||
var sw = b.getSouthWest(), ne = b.getNorthEast();
|
||||
return new maplibregl.LngLatBounds([sw.lng, sw.lat], [ne.lng, ne.lat]);
|
||||
}
|
||||
if (!coords || !coords.length) return null;
|
||||
var bb = new maplibregl.LngLatBounds();
|
||||
coords.forEach(function (c) { bb.extend(_ll(c)); });
|
||||
return bb;
|
||||
}
|
||||
|
||||
// ---- Marker-Wrapper (HTML-Marker; svgMarker + circleMarker) ----
|
||||
function _wrapMarker(lat, lon, el, anchor) {
|
||||
var m = new maplibregl.Marker({ element: el, anchor: anchor || 'center' }).setLngLat([lon, lat]);
|
||||
var wrap = {
|
||||
_gl: m,
|
||||
_el: el,
|
||||
addTo: function (mapWrap) { m.addTo(mapWrap && mapWrap._gl ? mapWrap._gl : mapWrap); return this; },
|
||||
bindPopup: function (html, opts) {
|
||||
m.setPopup(new maplibregl.Popup({ maxWidth: (opts && opts.maxWidth ? opts.maxWidth + 'px' : '260px'), closeButton: true, offset: 18 }).setHTML(html));
|
||||
return this;
|
||||
},
|
||||
openPopup: function () { var p = m.getPopup(); if (p && !p.isOpen()) m.togglePopup(); return this; },
|
||||
closePopup: function () { var p = m.getPopup(); if (p && p.isOpen()) m.togglePopup(); return this; },
|
||||
bindTooltip: function (t) { try { el.title = typeof t === 'string' ? t.replace(/<[^>]*>/g, '') : ''; } catch (e) {} return this; },
|
||||
on: function (ev, fn) {
|
||||
if (ev === 'click') el.addEventListener('click', function (e) { e.stopPropagation(); fn(e); });
|
||||
return this;
|
||||
},
|
||||
setLatLng: function (latlon) { m.setLngLat([latlon[1], latlon[0]]); return this; },
|
||||
setOpacity: function (o) { el.style.opacity = o; return this; },
|
||||
remove: function () { try { m.remove(); } catch (e) {} return this; },
|
||||
};
|
||||
return wrap;
|
||||
}
|
||||
|
||||
// Element aus HTML-String (für svgMarker mit custom HTML).
|
||||
function _elFromHtml(html, size, anchorY) {
|
||||
var wrap = document.createElement('div');
|
||||
wrap.innerHTML = html;
|
||||
var el = wrap.firstElementChild || wrap;
|
||||
el.style.cursor = 'pointer';
|
||||
return el;
|
||||
}
|
||||
|
||||
window.MapGLMini = {
|
||||
createMap: function (container, opts) {
|
||||
opts = opts || {};
|
||||
var el = typeof container === 'string' ? document.getElementById(container) : container;
|
||||
var center = opts.center || [51.1657, 10.4515];
|
||||
var map = new maplibregl.Map({
|
||||
container: el,
|
||||
style: MapGLStyle.build({ dark: !!opts.dark }),
|
||||
center: _ll(center), zoom: opts.zoom != null ? opts.zoom : 6,
|
||||
attributionControl: false, dragRotate: false, pitchWithRotate: false, maxZoom: 19,
|
||||
});
|
||||
map.touchZoomRotate.disableRotation();
|
||||
map.touchPitch.disable();
|
||||
try { el.style.touchAction = 'none'; } catch (e) {}
|
||||
if (opts.zoomControl !== false) map.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-left');
|
||||
map.addControl(new maplibregl.AttributionControl({
|
||||
compact: true, customAttribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
}));
|
||||
return _wrapMap(map);
|
||||
},
|
||||
|
||||
// svgMarker: custom HTML-Icon. opts: { size, anchorY }
|
||||
svgMarker: function (lat, lon, html, opts) {
|
||||
opts = opts || {};
|
||||
var el = _elFromHtml(html);
|
||||
// anchorY: Pixel von oben zum Ankerpunkt (Leaflet iconAnchor). 'bottom' wenn anchorY≈size.
|
||||
var anchor = 'center';
|
||||
if (opts.anchorY != null && opts.size) {
|
||||
anchor = opts.anchorY >= opts.size * 0.8 ? 'bottom' : 'center';
|
||||
}
|
||||
return _wrapMarker(lat, lon, el, anchor);
|
||||
},
|
||||
|
||||
circleMarker: function (lat, lon, opts) {
|
||||
opts = opts || {};
|
||||
var r = opts.radius || 8;
|
||||
var el = document.createElement('div');
|
||||
el.style.cssText = 'width:' + (r * 2) + 'px;height:' + (r * 2) + 'px;border-radius:50%;background:' +
|
||||
(opts.fillColor || opts.color || '#3B82F6') + ';border:' + (opts.weight || 2) + 'px solid ' +
|
||||
(opts.color || '#fff') + ';opacity:' + (opts.fillOpacity != null ? opts.fillOpacity : 1) +
|
||||
';box-shadow:0 1px 4px rgba(0,0,0,.35);cursor:pointer';
|
||||
return _wrapMarker(lat, lon, el, 'center');
|
||||
},
|
||||
|
||||
// featureGroup: nur als Bounds-Container (markers = Array von Wrappern mit _gl.getLngLat()).
|
||||
featureGroup: function (markers) {
|
||||
var coords = (markers || []).map(function (m) {
|
||||
var ll = m && m._gl && m._gl.getLngLat ? m._gl.getLngLat() : null;
|
||||
return ll ? [ll.lat, ll.lng] : null;
|
||||
}).filter(Boolean);
|
||||
return { _coords: coords, getBounds: function () { return { _coords: coords }; }, addTo: function () { return this; } };
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
|
@ -178,9 +178,9 @@ window.Page_lost = (() => {
|
|||
}
|
||||
|
||||
function _showUserOnMap() {
|
||||
if (!_map || !window.L || !_userPos) return;
|
||||
if (!_map || !_userPos) return;
|
||||
if (_userMarker) _map.removeLayer(_userMarker);
|
||||
_userMarker = L.circleMarker([_userPos.lat, _userPos.lon], {
|
||||
_userMarker = UI.map.circleMarker(_userPos.lat, _userPos.lon, {
|
||||
radius : 9,
|
||||
fillColor : '#3498db',
|
||||
color : '#fff',
|
||||
|
|
@ -266,7 +266,7 @@ window.Page_lost = (() => {
|
|||
// KARTEN-MARKER
|
||||
// ----------------------------------------------------------
|
||||
function _renderMarkers() {
|
||||
if (!_map || !window.L) return;
|
||||
if (!_map) return;
|
||||
_markers.forEach(m => _map.removeLayer(m));
|
||||
_markers = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -143,9 +143,9 @@ window.Page_poison = (() => {
|
|||
}
|
||||
|
||||
function _showUserOnMap() {
|
||||
if (!_map || !window.L || !_userPos) return;
|
||||
if (!_map || !_userPos) return;
|
||||
if (_userMarker) _map.removeLayer(_userMarker);
|
||||
_userMarker = L.circleMarker([_userPos.lat, _userPos.lon], {
|
||||
_userMarker = UI.map.circleMarker(_userPos.lat, _userPos.lon, {
|
||||
radius : 9,
|
||||
fillColor : '#3498db',
|
||||
color : '#fff',
|
||||
|
|
@ -201,7 +201,7 @@ window.Page_poison = (() => {
|
|||
// KARTEN-MARKER
|
||||
// ----------------------------------------------------------
|
||||
function _renderMarkers() {
|
||||
if (!_map || !window.L) return;
|
||||
if (!_map) return;
|
||||
_markers.forEach(m => _map.removeLayer(m));
|
||||
_markers = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -446,6 +446,20 @@ const UI = (() => {
|
|||
attributionControl = false,
|
||||
darkFilter = false,
|
||||
} = options;
|
||||
|
||||
// MapLibre-GL-Seitenkarte (gleicher Style wie die Hauptkarte) — hinter by_map_gl-Flag.
|
||||
if (_uiUseGL()) {
|
||||
try {
|
||||
await loadMapLibreUI();
|
||||
_uiGL = true;
|
||||
const isDark = document.documentElement.dataset.theme === 'dark';
|
||||
return MapGLMini.createMap(containerId, { center, zoom, zoomControl, dark: isDark });
|
||||
} catch (e) {
|
||||
console.warn('GL-Seitenkarte nicht verfügbar — Fallback Leaflet:', e);
|
||||
}
|
||||
}
|
||||
_uiGL = false;
|
||||
|
||||
await loadLeaflet();
|
||||
const m = L.map(containerId, { zoomControl, attributionControl }).setView(center, zoom);
|
||||
|
||||
|
|
@ -483,6 +497,7 @@ const UI = (() => {
|
|||
|
||||
// SVG-Marker mit eigenem HTML (z.B. mit Pulse-Animation, Rotation, etc.)
|
||||
svgMarker(lat, lon, html, { size = 32, anchorY = null, className = '' } = {}) {
|
||||
if (_uiGL && window.MapGLMini) return MapGLMini.svgMarker(lat, lon, html, { size, anchorY });
|
||||
const icon = L.divIcon({
|
||||
className,
|
||||
html,
|
||||
|
|
@ -492,6 +507,18 @@ const UI = (() => {
|
|||
return L.marker([lat, lon], { icon });
|
||||
},
|
||||
|
||||
// Engine-neutral: Kreis-Marker (Leaflet L.circleMarker bzw. GL-HTML-Punkt).
|
||||
circleMarker(lat, lon, opts = {}) {
|
||||
if (_uiGL && window.MapGLMini) return MapGLMini.circleMarker(lat, lon, opts);
|
||||
return L.circleMarker([lat, lon], opts);
|
||||
},
|
||||
|
||||
// Engine-neutral: FeatureGroup (nur als Bounds-Container für fitBounds genutzt).
|
||||
featureGroup(markers = []) {
|
||||
if (_uiGL && window.MapGLMini) return MapGLMini.featureGroup(markers);
|
||||
return L.featureGroup(markers);
|
||||
},
|
||||
|
||||
// Feature-Flag-Status der Vektor-Basemap (für Karten, die ihren Basemap-Layer
|
||||
// selbst verwalten, z.B. pages/map.js).
|
||||
vectorEnabled() { return _vectorMapEnabled(); },
|
||||
|
|
@ -846,7 +873,47 @@ const UI = (() => {
|
|||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// VEKTOR-BASEMAP (protomaps-leaflet + eigene PMTiles) — lazy laden
|
||||
// MapLibre-GL für Seitenkarten (UI.map) — lazy laden + Facade
|
||||
// ----------------------------------------------------------
|
||||
let _uiGL = false; // ist die aktuell erstellte UI-Karte GL?
|
||||
let _maplibreUIPromise = null;
|
||||
|
||||
// Gleiche Logik wie pages/map.js _useGL: Staging-Default AN, Prod AUS, by_map_gl überschreibt.
|
||||
function _uiUseGL() {
|
||||
try {
|
||||
const flag = localStorage.getItem('by_map_gl');
|
||||
if (flag === '1') return true;
|
||||
if (flag === '0') return false;
|
||||
return /(^|\.)staging\.banyaro\.app$/.test(location.hostname);
|
||||
} catch (e) { return false; }
|
||||
}
|
||||
|
||||
function loadMapLibreUI() {
|
||||
if (_maplibreUIPromise) return _maplibreUIPromise;
|
||||
const v = '?v=' + (window.APP_VER || '');
|
||||
if (!document.querySelector('link[href*="maplibre-gl.css"]')) {
|
||||
const l = document.createElement('link');
|
||||
l.rel = 'stylesheet'; l.href = '/js/vendor/maplibre-gl.css';
|
||||
document.head.appendChild(l);
|
||||
}
|
||||
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) ||
|
||||
(src.includes('map-gl-mini') && window.MapGLMini)) return res();
|
||||
const s = document.createElement('script');
|
||||
s.src = src + v; s.onload = res; s.onerror = rej;
|
||||
document.head.appendChild(s);
|
||||
})), Promise.resolve());
|
||||
_maplibreUIPromise = seq(['/js/vendor/maplibre-gl.js', '/js/vendor/pmtiles.js', '/js/map-gl-style.js', '/js/map-gl-mini.js']).then(() => {
|
||||
if (!(window.maplibregl && window.pmtiles && window.MapGLStyle && window.MapGLMini)) throw new Error('MapLibre (UI) nicht geladen');
|
||||
try { const proto = new pmtiles.Protocol(); maplibregl.addProtocol('pmtiles', proto.tile); } catch (e) { /* evtl. schon registriert */ }
|
||||
});
|
||||
return _maplibreUIPromise;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// VEKTOR-BASEMAP (protomaps-leaflet + eigene PMTiles) — lazy laden [DEAKTIVIERT]
|
||||
// ----------------------------------------------------------
|
||||
let _protomapsPromise = null;
|
||||
function loadProtomaps() {
|
||||
|
|
@ -905,9 +972,10 @@ const UI = (() => {
|
|||
// ----------------------------------------------------------
|
||||
function leafletMarker({ lat, lon, color = 'var(--c-primary)', icon = '', size = 32, label = '' } = {}) {
|
||||
const inner = label || icon;
|
||||
const html = `<div style="background:${color};color:#fff;font-size:${Math.round(size * 0.45)}px;font-weight:700;width:${size}px;height:${size}px;border-radius:50%;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 5px rgba(0,0,0,0.3);border:2px solid rgba(255,255,255,0.8)">${inner}</div>`;
|
||||
if (_uiGL && window.MapGLMini) return MapGLMini.svgMarker(lat, lon, html, { size, anchorY: size / 2 });
|
||||
const divIcon = L.divIcon({
|
||||
className: '',
|
||||
html: `<div style="background:${color};color:#fff;font-size:${Math.round(size * 0.45)}px;font-weight:700;width:${size}px;height:${size}px;border-radius:50%;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 5px rgba(0,0,0,0.3);border:2px solid rgba(255,255,255,0.8)">${inner}</div>`,
|
||||
className: '', html,
|
||||
iconSize: [size, size],
|
||||
iconAnchor: [size / 2, size / 2],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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=1194"></script>
|
||||
<script src="/js/landing-init.js?v=1195"></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 = '1194';
|
||||
const VER = '1195';
|
||||
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