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:
parent
a561759034
commit
2b5afcf0ae
4 changed files with 205 additions and 6 deletions
44
Makefile
44
Makefile
|
|
@ -28,7 +28,7 @@ TAR_EXCLUDE := --exclude='.git' \
|
|||
--exclude='./.DS_Store'
|
||||
|
||||
.PHONY: help deploy deploy-clean staging release sync push restart build stop status \
|
||||
logs logs-f shell db dev clean-cache check-ssh reports bump test
|
||||
logs logs-f shell db dev clean-cache check-ssh reports bump test tiles tiles-deploy
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# SSH-Prüfung — Abhängigkeit aller DS-Befehle
|
||||
|
|
@ -140,6 +140,48 @@ staging-db: check-ssh
|
|||
sudo chmod 666 $(DS_PATH_STAGING)/data/banyaro.db && \
|
||||
echo '✓ DB kopiert'"
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# TILES — DACH-Vektortiles (planetiler → PMTiles), lokal bauen + ausliefern
|
||||
# Voraussetzung: Docker Desktop läuft, osmium installiert (brew install osmium-tool).
|
||||
# make tiles DACH neu generieren (download → merge → planetiler)
|
||||
# make tiles-deploy dach.pmtiles auf Staging ausliefern (atomar)
|
||||
# make tiles-deploy ENV=prod dach.pmtiles auf Produktion ausliefern (atomar)
|
||||
# Monatlich neu generieren hält die Karte aktuell. Datei liegt im data-Volume,
|
||||
# NICHT im Image — wird per Range-Route (/tiles) ausgeliefert.
|
||||
# ----------------------------------------------------------
|
||||
TILES_DIR := tiles/build
|
||||
TILES_REGIONS := germany austria switzerland
|
||||
PLANETILER_IMAGE := ghcr.io/onthegomap/planetiler:latest
|
||||
TILES_TARGET := $(if $(filter prod,$(ENV)),$(DS_PATH),$(DS_PATH_STAGING))
|
||||
|
||||
tiles:
|
||||
@mkdir -p $(TILES_DIR)
|
||||
@echo "→ Geofabrik-Extrakte laden ($(TILES_REGIONS))..."
|
||||
@for r in $(TILES_REGIONS); do \
|
||||
echo " $$r"; \
|
||||
curl -fsSL -o $(TILES_DIR)/$$r.osm.pbf https://download.geofabrik.de/europe/$$r-latest.osm.pbf; done
|
||||
@echo "→ merge (History) + time-filter dedup → dach.osm.pbf..."
|
||||
@# Geofabrik-Extrakte können versetzte Stände haben (z.B. germany älter als at/ch) →
|
||||
@# Grenz-Nodes mit abweichender Version. Als History mergen + auf 'jetzt' snapshotten
|
||||
@# liefert genau eine Version pro ID (planetiler braucht eindeutige, sortierte IDs).
|
||||
@osmium merge -H $(foreach r,$(TILES_REGIONS),$(TILES_DIR)/$(r).osm.pbf) -o $(TILES_DIR)/dach-hist.osm.pbf --overwrite
|
||||
@osmium time-filter $(TILES_DIR)/dach-hist.osm.pbf -o $(TILES_DIR)/dach.osm.pbf --overwrite
|
||||
@rm -f $(TILES_DIR)/dach-hist.osm.pbf
|
||||
@echo "→ planetiler → dach.pmtiles (disk-backed mmap)..."
|
||||
@docker run --rm -v "$(CURDIR)/$(TILES_DIR):/data" $(PLANETILER_IMAGE) \
|
||||
--osm-path=/data/dach.osm.pbf --download --output=/data/dach.pmtiles --force \
|
||||
--storage=mmap --nodemap-storage=mmap
|
||||
@echo ""
|
||||
@echo " ✓ Tiles gebaut:"; ls -lh $(TILES_DIR)/dach.pmtiles
|
||||
|
||||
tiles-deploy: check-ssh
|
||||
@if [ ! -f $(TILES_DIR)/dach.pmtiles ]; then echo "❌ $(TILES_DIR)/dach.pmtiles fehlt — erst 'make tiles'"; exit 1; fi
|
||||
@echo "→ Ausliefern nach $(TILES_TARGET)/data/tiles/ (atomarer Swap)..."
|
||||
@ssh $(DS_HOST) "mkdir -p $(TILES_TARGET)/data/tiles"
|
||||
@scp -O $(TILES_DIR)/dach.pmtiles $(DS_HOST):$(TILES_TARGET)/data/tiles/dach.pmtiles.tmp
|
||||
@ssh $(DS_HOST) "mv -f $(TILES_TARGET)/data/tiles/dach.pmtiles.tmp $(TILES_TARGET)/data/tiles/dach.pmtiles"
|
||||
@echo " ✓ dach.pmtiles ausgeliefert ($(if $(filter prod,$(ENV)),PRODUKTION,Staging))"
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# RELEASE — develop → main → Production (VERSION= pflichtangabe)
|
||||
# Beispiel: make release VERSION=1.1.0
|
||||
|
|
|
|||
92
backend/static/js/map-vector.js
Normal file
92
backend/static/js/map-vector.js
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
// Vektor-Basemap für Leaflet via protomaps-leaflet, gerendert aus unseren eigenen
|
||||
// PMTiles (OpenMapTiles-Schema von planetiler, ausgeliefert unter /tiles/).
|
||||
// Ersetzt den OSM-Raster-Layer — Leaflet + markercluster + alle Marker bleiben unberührt.
|
||||
// Labels werden von protomaps-leaflet per Canvas-Text gezeichnet → KEINE Glyphs nötig.
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// Single-File-Tile-Archiv (DACH). Liegt im data-Volume, per Range ausgeliefert.
|
||||
var TILES_FILE = 'dach.pmtiles';
|
||||
|
||||
function tilesUrl() {
|
||||
return window.location.origin + '/tiles/' + TILES_FILE;
|
||||
}
|
||||
|
||||
// Straßenbreite zoomabhängig (dünn weit draußen, breit im Detail).
|
||||
function roadWidth(z) {
|
||||
if (z >= 16) return 4;
|
||||
if (z >= 14) return 2.5;
|
||||
if (z >= 12) return 1.5;
|
||||
if (z >= 9) return 0.8;
|
||||
return 0.4;
|
||||
}
|
||||
|
||||
// Farbpaletten Light/Dark.
|
||||
var THEMES = {
|
||||
light: {
|
||||
bg: '#f4f1ec', water: '#a0c8f0', land: '#dce8c8', park: '#c8e6b0',
|
||||
road: '#ffffff', roadCasing: '#d9cfc2', building: '#e6ddcf',
|
||||
buildingLine: '#d4cabb', boundary: '#b08ac0',
|
||||
label: '#33312e', labelHalo: 'rgba(255,255,255,.85)',
|
||||
},
|
||||
dark: {
|
||||
bg: '#1a1d21', water: '#16242e', land: '#222820', park: '#27331f',
|
||||
road: '#3a4046', roadCasing: '#23282d', building: '#262b30',
|
||||
buildingLine: '#31373d', boundary: '#7d5a8c',
|
||||
label: '#cfd2d6', labelHalo: 'rgba(0,0,0,.8)',
|
||||
},
|
||||
};
|
||||
|
||||
function buildRules(t) {
|
||||
var P = protomapsL;
|
||||
var paint = [
|
||||
{ dataLayer: 'landcover',
|
||||
symbolizer: new P.PolygonSymbolizer({ fill: t.land, opacity: 0.55 }) },
|
||||
{ dataLayer: 'park',
|
||||
symbolizer: new P.PolygonSymbolizer({ fill: t.park, opacity: 0.5 }) },
|
||||
{ dataLayer: 'water',
|
||||
symbolizer: new P.PolygonSymbolizer({ fill: t.water }) },
|
||||
{ dataLayer: 'waterway',
|
||||
symbolizer: new P.LineSymbolizer({ color: t.water, width: 0.8 }) },
|
||||
// Straßen-Casing zuerst (liegt unter der Füllung), dann die Straße.
|
||||
{ dataLayer: 'transportation', minzoom: 11,
|
||||
symbolizer: new P.LineSymbolizer({ color: t.roadCasing,
|
||||
width: function (z) { return roadWidth(z) + 1.5; } }) },
|
||||
{ dataLayer: 'transportation',
|
||||
symbolizer: new P.LineSymbolizer({ color: t.road, width: roadWidth }) },
|
||||
{ dataLayer: 'building', minzoom: 13,
|
||||
symbolizer: new P.PolygonSymbolizer({ fill: t.building, stroke: t.buildingLine, width: 0.5 }) },
|
||||
{ dataLayer: 'boundary',
|
||||
symbolizer: new P.LineSymbolizer({ color: t.boundary, width: 1, dash: [2, 2] }) },
|
||||
];
|
||||
var label = [
|
||||
{ dataLayer: 'place', minzoom: 4,
|
||||
symbolizer: new P.CenteredTextSymbolizer({
|
||||
labelProps: ['name:de', 'name'],
|
||||
fill: t.label, stroke: t.labelHalo, width: 2.5,
|
||||
font: function (z) { return (z >= 10 ? '600 13px' : '600 11px') + ' system-ui, sans-serif'; },
|
||||
}) },
|
||||
];
|
||||
return { paint: paint, label: label };
|
||||
}
|
||||
|
||||
// Erzeugt den Leaflet-Layer. dark=true → dunkles Theme.
|
||||
function basemapLayer(opts) {
|
||||
opts = opts || {};
|
||||
var t = THEMES[opts.dark ? 'dark' : 'light'];
|
||||
var rules = buildRules(t);
|
||||
return protomapsL.leafletLayer({
|
||||
url: tilesUrl(),
|
||||
paintRules: rules.paint,
|
||||
labelRules: rules.label,
|
||||
backgroundColor: t.bg,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
});
|
||||
}
|
||||
|
||||
window.MapVector = {
|
||||
basemapLayer: basemapLayer,
|
||||
tilesUrl: tilesUrl,
|
||||
tilesFile: TILES_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:
|
||||
|
|
|
|||
9
backend/static/js/vendor/protomaps-leaflet.js
vendored
Normal file
9
backend/static/js/vendor/protomaps-leaflet.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue