Tile-Server: Vektor-Basemap in PWA integrieren (protomaps-leaflet, Feature-Flag)

- ui.js Map.create: Basemap-Swap OSM-Raster→PMTiles-Vektorlayer hinter Flag
  'by_vector_map' (?vectormap=1/0). Leaflet+markercluster+Marker unverändert,
  sauberer Raster-Fallback bei Fehler. Attribution Pflicht eingeblendet.
- map-vector.js: protomaps-leaflet paintRules/labelRules für OpenMapTiles-Schema
  (Light+Dark), Labels per Canvas-Text → keine Glyphs nötig. Quelle /tiles/dach.pmtiles.
- protomaps-leaflet 4.0.1 vendored.
- Makefile: 'make tiles' (download→merge -H+time-filter dedup→planetiler) + 'make tiles-deploy'
  (atomarer Swap, ENV=prod für Produktion).
This commit is contained in:
rene 2026-06-04 21:53:07 +02:00
parent a561759034
commit 2b5afcf0ae
4 changed files with 205 additions and 6 deletions

View file

@ -439,7 +439,6 @@ const UI = (() => {
OSM_MAX_ZOOM: 19,
async create(containerId, options = {}) {
await loadLeaflet();
const {
center = [51.1657, 10.4515],
zoom = 6,
@ -447,11 +446,33 @@ const UI = (() => {
attributionControl = false,
darkFilter = false,
} = options;
await loadLeaflet();
const m = L.map(containerId, { zoomControl, attributionControl }).setView(center, zoom);
const tiles = L.tileLayer(this.OSM_URL, { maxZoom: this.OSM_MAX_ZOOM }).addTo(m);
if (darkFilter) {
const isDark = document.documentElement.dataset.theme === 'dark';
if (isDark) tiles.getContainer().style.filter = 'brightness(0.7) invert(1) contrast(0.9) hue-rotate(200deg)';
// Vektor-Basemap aus eigenen PMTiles (hinter Feature-Flag). Bei Fehler
// (Tiles/Lib nicht da) sauberer Fallback auf den OSM-Raster — Marker etc.
// bleiben in beiden Fällen identisch (reiner Basemap-Tausch).
let usedVector = false;
if (_vectorMapEnabled()) {
try {
await loadProtomaps();
const isDark = document.documentElement.dataset.theme === 'dark';
MapVector.basemapLayer({ dark: isDark }).addTo(m);
if (!attributionControl) {
L.control.attribution({ prefix: false }).addTo(m)
.addAttribution('© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors');
}
usedVector = true;
} catch (e) {
console.warn('Vektor-Basemap nicht verfügbar — Fallback auf Raster:', e);
}
}
if (!usedVector) {
const tiles = L.tileLayer(this.OSM_URL, { maxZoom: this.OSM_MAX_ZOOM }).addTo(m);
if (darkFilter) {
const isDark = document.documentElement.dataset.theme === 'dark';
if (isDark) tiles.getContainer().style.filter = 'brightness(0.7) invert(1) contrast(0.9) hue-rotate(200deg)';
}
}
// Safety-Net: Container-Größe nach Layout neu vermessen. Verhindert
// grau bleibende Bereiche wenn die Karte vor dem finalen Layout erstellt
@ -813,6 +834,41 @@ const UI = (() => {
});
}
// ----------------------------------------------------------
// VEKTOR-BASEMAP (protomaps-leaflet + eigene PMTiles) — lazy laden
// ----------------------------------------------------------
let _protomapsPromise = null;
function loadProtomaps() {
if (_protomapsPromise) return _protomapsPromise;
const v = '?v=' + (window.APP_VER || '');
const loadSeq = (srcs) => srcs.reduce((p, src) => p.then(() => new Promise((res, rej) => {
if ((src.includes('protomaps-leaflet') && window.protomapsL) ||
(src.includes('map-vector') && window.MapVector)) return res();
const s = document.createElement('script');
s.src = src + v;
s.onload = res; s.onerror = rej;
document.head.appendChild(s);
})), Promise.resolve());
// map-vector.js hängt von protomapsL ab → strikt sequenziell laden.
_protomapsPromise = loadSeq(['/js/vendor/protomaps-leaflet.js', '/js/map-vector.js'])
.then(() => {
if (window.protomapsL && window.MapVector) return;
throw new Error('protomaps-leaflet/MapVector nicht geladen');
});
return _protomapsPromise;
}
// Feature-Flag: localStorage 'by_vector_map'==='1'. ?vectormap=1/0 setzt ihn (Testing).
function _vectorMapEnabled() {
try {
const u = new URLSearchParams(location.search);
if (u.has('vectormap')) {
localStorage.setItem('by_vector_map', u.get('vectormap') === '0' ? '0' : '1');
}
return localStorage.getItem('by_vector_map') === '1';
} catch (e) { return false; }
}
// ----------------------------------------------------------
// LEAFLET MARKER FACTORY — erzeugt einen L.divIcon-Marker
// Verwendung: