Compare commits

...

2 commits

Author SHA1 Message Date
9006c85434 Bump APP_VER 1173→1174 (Vektor-Basemap-Integration) 2026-06-04 22:26:42 +02:00
2b5afcf0ae 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).
2026-06-04 21:53:07 +02:00
9 changed files with 221 additions and 22 deletions

View file

@ -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

View file

@ -1 +1 @@
1173
1174

View file

@ -86,14 +86,14 @@
<title>Ban Yaro</title>
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
<script src="/js/boot-early.js?v=1173"></script>
<script src="/js/boot-early.js?v=1174"></script>
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
<link rel="stylesheet" href="/css/design-system.css?v=1173">
<link rel="stylesheet" href="/css/layout.css?v=1173">
<link rel="stylesheet" href="/css/components.css?v=1173">
<link rel="stylesheet" href="/css/utilities.css?v=1173">
<link rel="stylesheet" href="/css/lists.css?v=1173">
<link rel="stylesheet" href="/css/design-system.css?v=1174">
<link rel="stylesheet" href="/css/layout.css?v=1174">
<link rel="stylesheet" href="/css/components.css?v=1174">
<link rel="stylesheet" href="/css/utilities.css?v=1174">
<link rel="stylesheet" href="/css/lists.css?v=1174">
</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=1173"></script>
<script src="/js/ui.js?v=1173"></script>
<script src="/js/app.js?v=1173"></script>
<script src="/js/worlds.js?v=1173"></script>
<script src="/js/offline-indicator.js?v=1173"></script>
<script src="/js/api.js?v=1174"></script>
<script src="/js/ui.js?v=1174"></script>
<script src="/js/app.js?v=1174"></script>
<script src="/js/worlds.js?v=1174"></script>
<script src="/js/offline-indicator.js?v=1174"></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=1173"></script>
<script src="/js/boot.js?v=1174"></script>
</body>

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '1173'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '1174'; // ← 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;

View 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,
};
})();

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,12 +446,34 @@ const UI = (() => {
attributionControl = false,
darkFilter = false,
} = options;
await loadLeaflet();
const m = L.map(containerId, { zoomControl, attributionControl }).setView(center, zoom);
// 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
// wird (z.B. in frisch eingefügten Overlays mit flex:1).
@ -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:

File diff suppressed because one or more lines are too long

View file

@ -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=1173"></script>
<script src="/js/landing-init.js?v=1174"></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">

View file

@ -4,7 +4,7 @@
============================================================ */
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
const VER = '1173';
const VER = '1174';
const CACHE_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten