- by_offline_tiles Default AN auf staging.banyaro.app (localStorage/?tilesoffline=1/0 uebersteuert) - Speed-Dial 'Karte offline speichern': GL -> MapOffline.downloadAround(Kartenmitte, 5km), Leaflet -> alter Raster-Prefetch (_cacheTiles war seit FAB-Redesign verwaist) - Glyphs in IndexedDB (Key-Praefix f/) + byt://f/-Protokoll: ueberlebt App-Updates - OSM-Raster-Prefetch im Offline-Tiles-Modus uebersprungen (GL nutzt das Raster nicht) - Button-Sichtbarkeit gated: GL ohne Offline-Flag (= Production) zeigt ihn nicht
186 lines
12 KiB
JavaScript
186 lines
12 KiB
JavaScript
// MapLibre-GL-Style für die zentrale Karte — gerendert aus unseren DACH-PMTiles
|
|
// (OpenMapTiles-Schema). GPU + Worker → performant auf dem Handy (Ziel der Migration).
|
|
// GEOMETRIE-ONLY (keine Symbol/Text-Layer) → KEINE Glyphs/Fonts nötig für den ersten
|
|
// Perf-Test. Labels (mit Glyph-Hosting) kommen in M3, wenn die Performance steht.
|
|
(function () {
|
|
'use strict';
|
|
|
|
var TILES_FILE = 'dach.pmtiles';
|
|
// Cache-Bust: gleiche Datei-URL, aber Inhalt ändert sich bei jedem Tile-Deploy (atomarer Swap).
|
|
// Ohne Versions-Param liefert der Browser bis zu 24h die ALTEN PMTiles-Bytes (inkl. altem Directory)
|
|
// → man sähe die alte Abdeckung. Bei jedem `make tiles-deploy` HOCHZÄHLEN (Makefile sed't das).
|
|
var TILES_VER = '20260605';
|
|
function tilesUrl() { return window.location.origin + '/tiles/' + TILES_FILE + '?v=' + TILES_VER; }
|
|
|
|
// Offline-Tiles-Modus (byt://-Quelle). localStorage by_offline_tiles bzw. ?tilesoffline=1/0 übersteuert.
|
|
// Staging-Default AN seit 2026-06-06 (Gerätetest); Production bleibt AUS bis Freigabe (dann analog by_map_gl).
|
|
// ACHTUNG: Default-Logik synchron halten mit offline-indicator.js _offlineTilesMode().
|
|
function _offlineEnabled() {
|
|
try {
|
|
var flag = localStorage.getItem('by_offline_tiles');
|
|
if (flag === '1') return true;
|
|
if (flag === '0') return false;
|
|
return location.hostname === 'staging.banyaro.app';
|
|
} catch (e) { return false; }
|
|
}
|
|
|
|
var THEMES = {
|
|
light: {
|
|
bg: '#f2efe8', land: '#cbe3a8', park: '#aedd88', water: '#7fbbe8',
|
|
forest: '#74b356', grass: '#cdeaa6', wetland: '#9ed2bc', farmland: '#e7eecb', sand: '#efe6c8', parkLine: '#4e9a3a',
|
|
road: '#ffffff', roadMotorway: '#e89aa0', roadTrunk: '#efb188', roadPrimary: '#f4cf92', roadSecondary: '#efe79c',
|
|
roadCasing: '#cdbfa9', building: '#e6d8bf',
|
|
buildingLine: '#cdbb9c', boundary: '#a06ec0', path: '#b08160', rail: '#9a9aa2',
|
|
label: '#2a2823', roadLabel: '#574f43', waterLabel: '#2f6aa0', poiLabel: '#4a4236', labelHalo: 'rgba(255,255,255,0.95)',
|
|
},
|
|
dark: {
|
|
bg: '#1a1d21', land: '#252e1d', park: '#2c3c1f', water: '#163242',
|
|
forest: '#33501f', grass: '#26361a', wetland: '#264039', farmland: '#222b18', sand: '#332f1f', parkLine: '#3f6e2a',
|
|
road: '#444a52', roadMotorway: '#70454b', roadTrunk: '#6e533c', roadPrimary: '#6b5e3a', roadSecondary: '#565232',
|
|
roadCasing: '#23282d', building: '#2a2f35',
|
|
buildingLine: '#373d44', boundary: '#8a63a0', path: '#6b5d52', rail: '#5e5e68',
|
|
label: '#e2e5e9', roadLabel: '#a6acb3', waterLabel: '#7db0dd', poiLabel: '#c3b9a8', labelHalo: 'rgba(0,0,0,0.85)',
|
|
},
|
|
};
|
|
|
|
var FONT = ['Open Sans Regular'];
|
|
var FONT_BOLD = ['Open Sans Semibold'];
|
|
|
|
// Liefert ein MapLibre-Style-JSON (Version 8) ohne glyphs/sprite.
|
|
function build(opts) {
|
|
opts = opts || {};
|
|
var t = THEMES[opts.dark ? 'dark' : 'light'];
|
|
// offline → Tiles übers byt://-Protokoll (IndexedDB-first, remote-Fallback) statt direkt aus der
|
|
// Remote-PMTiles. Nötig für Offline-Betrieb. Default aus Flag (by_offline_tiles), explizit übersteuerbar.
|
|
var useOffline = opts.offline != null ? opts.offline : _offlineEnabled();
|
|
var src = useOffline
|
|
? { type: 'vector', tiles: ['byt://t/{z}/{x}/{y}'], minzoom: 0, maxzoom: 14 }
|
|
: { type: 'vector', url: 'pmtiles://' + tilesUrl() };
|
|
return {
|
|
version: 8,
|
|
// Offline-Modus: Glyphs übers byt://f/-Protokoll (IndexedDB-first, remote-Fallback) —
|
|
// der SW-Cache für /fonts wird bei App-Updates gepurged, IndexedDB nicht.
|
|
glyphs: useOffline
|
|
? 'byt://f/{fontstack}/{range}'
|
|
: window.location.origin + '/fonts/{fontstack}/{range}.pbf',
|
|
sources: {
|
|
by: src,
|
|
},
|
|
layers: [
|
|
{ id: 'bg', type: 'background', paint: { 'background-color': t.bg } },
|
|
// Landbedeckung nach Klasse: Wald dunkler, Wiese heller, Moor/Feuchtgebiet eigen.
|
|
{ id: 'landcover', type: 'fill', source: 'by', 'source-layer': 'landcover',
|
|
paint: {
|
|
'fill-color': ['match', ['get', 'class'],
|
|
'wood', t.forest, 'grass', t.grass, 'wetland', t.wetland,
|
|
'farmland', t.farmland, 'sand', t.sand, t.land],
|
|
'fill-opacity': 0.85,
|
|
} },
|
|
// Schutzgebiete/Naturparks: dezente Füllung + grüne gestrichelte Umrandung
|
|
// (NICHT aufhellend über den Wald legen → sonst wirkt z.B. der Ebersberger Forst heller).
|
|
{ id: 'park-fill', type: 'fill', source: 'by', 'source-layer': 'park',
|
|
paint: { 'fill-color': t.park, 'fill-opacity': 0.18 } },
|
|
{ id: 'park-outline', type: 'line', source: 'by', 'source-layer': 'park',
|
|
paint: { 'line-color': t.parkLine, 'line-width': 1.2, 'line-dasharray': [4, 2], 'line-opacity': 0.55 } },
|
|
{ id: 'water', type: 'fill', source: 'by', 'source-layer': 'water',
|
|
paint: { 'fill-color': t.water } },
|
|
{ id: 'waterway', type: 'line', source: 'by', 'source-layer': 'waterway',
|
|
paint: { 'line-color': t.water, 'line-width': 1 } },
|
|
// Pfade/Wege/Tracks: dünn + gestrichelt (NICHT wie Straßen).
|
|
{ id: 'paths', type: 'line', source: 'by', 'source-layer': 'transportation', minzoom: 13,
|
|
filter: ['in', ['get', 'class'], ['literal', ['path', 'track']]],
|
|
paint: { 'line-color': t.path, 'line-dasharray': [1.8, 1.8],
|
|
'line-width': ['interpolate', ['linear'], ['zoom'], 13, 0.6, 16, 1.2, 19, 2] } },
|
|
// Straßen-Casing (nur echte Straßen, Breite nach Klasse).
|
|
{ id: 'road-casing', type: 'line', source: 'by', 'source-layer': 'transportation', minzoom: 11,
|
|
filter: ['!', ['in', ['get', 'class'], ['literal', ['path', 'track', 'ferry', 'rail', 'transit', 'aerialway']]]],
|
|
paint: { 'line-color': t.roadCasing,
|
|
'line-width': ['interpolate', ['linear'], ['zoom'],
|
|
11, ['match', ['get', 'class'], ['motorway', 'trunk'], 3, ['primary', 'secondary'], 2.2, 1.6],
|
|
16, ['match', ['get', 'class'], ['motorway', 'trunk'], 8, ['primary', 'secondary'], 6, 4.5]] } },
|
|
// Straßen-Füllung.
|
|
{ id: 'roads', type: 'line', source: 'by', 'source-layer': 'transportation',
|
|
filter: ['!', ['in', ['get', 'class'], ['literal', ['path', 'track', 'ferry', 'rail', 'transit', 'aerialway']]]],
|
|
paint: { 'line-color': ['match', ['get', 'class'],
|
|
'motorway', t.roadMotorway, 'trunk', t.roadTrunk, 'primary', t.roadPrimary, 'secondary', t.roadSecondary, t.road],
|
|
'line-width': ['interpolate', ['linear'], ['zoom'],
|
|
6, ['match', ['get', 'class'], ['motorway', 'trunk'], 1.4, 0.4],
|
|
12, ['match', ['get', 'class'], ['motorway', 'trunk', 'primary'], 2.4, 1.1],
|
|
16, ['match', ['get', 'class'], ['motorway', 'trunk'], 6, ['primary', 'secondary'], 4.5, 3]] } },
|
|
// Bahntrassen: Basis-Linie + Schwellen (dicke gestrichelte Überlagerung).
|
|
{ id: 'railway', type: 'line', source: 'by', 'source-layer': 'transportation', minzoom: 11,
|
|
filter: ['in', ['get', 'class'], ['literal', ['rail', 'transit']]],
|
|
paint: { 'line-color': t.rail, 'line-width': ['interpolate', ['linear'], ['zoom'], 11, 0.8, 16, 2] } },
|
|
{ id: 'railway-ties', type: 'line', source: 'by', 'source-layer': 'transportation', minzoom: 13,
|
|
filter: ['in', ['get', 'class'], ['literal', ['rail', 'transit']]],
|
|
paint: { 'line-color': t.rail, 'line-dasharray': [0.35, 3],
|
|
'line-width': ['interpolate', ['linear'], ['zoom'], 13, 3, 16, 6] } },
|
|
{ id: 'buildings', type: 'fill', source: 'by', 'source-layer': 'building',
|
|
minzoom: 13,
|
|
paint: { 'fill-color': t.building, 'fill-outline-color': t.buildingLine } },
|
|
{ id: 'boundary', type: 'line', source: 'by', 'source-layer': 'boundary',
|
|
paint: { 'line-color': t.boundary, 'line-dasharray': [2, 2], 'line-width': 1 } },
|
|
|
|
// ---- Labels (brauchen glyphs). Reihenfolge = Kollisions-Priorität (zuerst = wichtiger). ----
|
|
// Ortsnamen (Städte/Dörfer) zuerst — höchste Priorität, auch bei kleinem Zoom.
|
|
{ id: 'place-labels', type: 'symbol', source: 'by', 'source-layer': 'place',
|
|
filter: ['in', ['get', 'class'], ['literal', ['city', 'town', 'village', 'suburb', 'hamlet', 'neighbourhood']]],
|
|
layout: {
|
|
'text-field': ['coalesce', ['get', 'name:de'], ['get', 'name']],
|
|
'text-font': FONT_BOLD, 'text-max-width': 8, 'text-anchor': 'center',
|
|
'text-size': ['interpolate', ['linear'], ['zoom'], 4, 10, 8, 12, 12, 14, 16, 17],
|
|
},
|
|
paint: { 'text-color': t.label, 'text-halo-color': t.labelHalo, 'text-halo-width': 1.6 } },
|
|
|
|
{ id: 'water-labels', type: 'symbol', source: 'by', 'source-layer': 'water_name',
|
|
layout: { 'text-field': ['coalesce', ['get', 'name:de'], ['get', 'name']], 'text-font': FONT_BOLD, 'text-size': 12, 'text-max-width': 6 },
|
|
paint: { 'text-color': t.waterLabel, 'text-halo-color': t.labelHalo, 'text-halo-width': 1.2 } },
|
|
|
|
{ id: 'street-labels', type: 'symbol', source: 'by', 'source-layer': 'transportation_name', minzoom: 14,
|
|
layout: { 'text-field': ['coalesce', ['get', 'name:de'], ['get', 'name']], 'text-font': FONT_BOLD, 'symbol-placement': 'line', 'text-size': 11 },
|
|
paint: { 'text-color': t.roadLabel, 'text-halo-color': t.labelHalo, 'text-halo-width': 1.2 } },
|
|
|
|
// Straßennummern (A9, B304, ST2078) entlang großer Straßen.
|
|
{ id: 'road-refs', type: 'symbol', source: 'by', 'source-layer': 'transportation_name', minzoom: 8,
|
|
filter: ['all', ['has', 'ref'], ['in', ['get', 'class'], ['literal', ['motorway', 'trunk', 'primary', 'secondary', 'tertiary']]]],
|
|
layout: {
|
|
'text-field': ['get', 'ref'], 'text-font': FONT_BOLD, 'text-size': 11,
|
|
'symbol-placement': 'line', 'symbol-spacing': 350, 'text-max-angle': 20,
|
|
'text-rotation-alignment': 'viewport', 'text-pitch-alignment': 'viewport',
|
|
},
|
|
paint: { 'text-color': t.label, 'text-halo-color': t.labelHalo, 'text-halo-width': 2.5 } },
|
|
|
|
// POI-Namen (Kinderspielplatz, Schule, …) ab Z15 — Kollisionserkennung verhindert Überladung.
|
|
{ id: 'poi-labels', type: 'symbol', source: 'by', 'source-layer': 'poi', minzoom: 15,
|
|
layout: {
|
|
'text-field': ['coalesce', ['get', 'name:de'], ['get', 'name']],
|
|
'text-font': FONT_BOLD, 'text-size': 11, 'text-max-width': 8,
|
|
'text-anchor': 'top', 'text-offset': [0, 0.4], 'symbol-sort-key': ['get', 'rank'],
|
|
},
|
|
paint: { 'text-color': t.poiLabel, 'text-halo-color': t.labelHalo, 'text-halo-width': 1.2 } },
|
|
|
|
// Hausnummern ab Z17 (niedrigste Priorität).
|
|
{ id: 'housenumbers', type: 'symbol', source: 'by', 'source-layer': 'housenumber', minzoom: 17,
|
|
layout: { 'text-field': ['get', 'housenumber'], 'text-font': FONT_BOLD, 'text-size': 9.5 },
|
|
paint: { 'text-color': t.roadLabel, 'text-halo-color': t.labelHalo, 'text-halo-width': 1 } },
|
|
],
|
|
};
|
|
}
|
|
|
|
// Compact-Attribution standardmäßig EINGEKLAPPT lassen (nur das ⓘ; der volle Text
|
|
// "© OpenStreetMap contributors" erscheint erst auf Klick). MapLibre rendert sie sonst
|
|
// offen (Klasse maplibregl-compact-show + open). Rechtlich reicht das ⓘ (ODbL).
|
|
function collapseAttribution(map) {
|
|
var fn = function () {
|
|
try {
|
|
var a = map.getContainer().querySelector('.maplibregl-ctrl-attrib');
|
|
if (a) { a.classList.remove('maplibregl-compact-show'); a.removeAttribute('open'); }
|
|
} catch (e) {}
|
|
};
|
|
fn();
|
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(fn);
|
|
setTimeout(fn, 60);
|
|
}
|
|
|
|
window.MapGLStyle = { build: build, tilesUrl: tilesUrl, tilesFile: TILES_FILE, collapseAttribution: collapseAttribution, offlineEnabled: _offlineEnabled };
|
|
})();
|