Standort: Feinschliff aus Claude Design übernehmen + Satellit/Straßenkarte-Umschalter

Übernimmt den in Claude Design überarbeiteten Stand der Standort-Funktion
(echte Audi-CI-Icons statt Platzhalter-SVGs, robustere Abstands-/Adress-
Anzeige, dynamisch gemessene Menü-Zugweg-Höhe) und ergänzt lokal den
Layer-Knopf so, dass er wie gewünscht zwischen Satellit (Esri World
Imagery, kostenlos ohne Key) und Straßenkarte umschaltet statt zwischen
Tag/Nacht-Theme.
This commit is contained in:
2026-08-12 12:32:34 +02:00
parent dc4c1ff0bf
commit 7e979ecfab
+124 -53
View File
@@ -384,6 +384,10 @@ const TILES = {
tag: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png",
};
const ATTR = "© OpenStreetMap · CARTO";
/* Satellitenansicht der Standort-Vollbildkarte (Layer-Knopf) - wie die
CARTO-Straßenkacheln oben ohne eigenen Schlüssel nutzbar. */
const SAT_TILE_URL = "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}";
const SAT_ATTR = "Tiles © Esri";
function leafletLaden() {
if (window.L) return Promise.resolve();
@@ -480,26 +484,24 @@ let USER_POS_FEHLER = null;
let STANDORT_ADRESSE = null; // zuletzt aufgelöste Adresse (Cache über Koordinaten-Rundung)
let STANDORT_ADRESSE_KEY = null;
let STANDORTMENU_OFFEN = false;
let SMAP = null, SMAP_TILE = null, SMAP_STIL = null; // Vollbild-Karte (eigenständig von MAP/TMAP)
let SMAP = null, SMAP_TILE = null, SMAP_STIL = null; // Vollbild-Karte (eigenständig von MAP/TMAP); SMAP_STIL: null/"strasse" oder "satellit"
let TMAP = null, TMAP_TILE = null; // Vorschau-Karte der Übersicht-Kachel
let FAHRZEUG_MARKER = null, USER_MARKER = null;
const STANDORT_PEEK_PX = 108; // muss zu --standort-peek in audi-dashboard.css passen
const STANDORT_PEEK_PX = 96; // sichtbarer Anteil im geschlossenen Zustand, muss zu --standort-peek in audi-dashboard.css passen
function standortBekannt() { return CAR.standortLat != null && CAR.standortLon != null; }
/* Fahrzeug-Symbol im myAudi-Stil: einfaches Top-down-Glyph, wiederverwendet
für Kachel-/Vollbild-Marker und den "Auf Fahrzeug zentrieren"-Knopf. */
/* Fahrzeug-Symbol im myAudi-Stil: einfaches Top-down-Glyph, noch fuer
Vorschauen in Gebrauch; die Kartenmarkierung nutzt poi-car (Audi CI). */
function fahrzeugGlyphPfade() {
return `<path d="M5 15.3 6.4 9.9a2 2 0 0 1 1.9-1.5h7.4a2 2 0 0 1 1.9 1.5l1.4 5.4"/>
<path d="M4.6 15.3h14.8a.9.9 0 0 1 .9.9v1.1a.9.9 0 0 1-.9.9H4.6a.9.9 0 0 1-.9-.9v-1.1a.9.9 0 0 1 .9-.9Z"/>
<path d="M7.4 11.4h9.2"/>`;
}
function fahrzeugMarkerSVG() {
return `<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="#fff" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
${fahrzeugGlyphPfade()}
<circle cx="7.6" cy="18.3" r="1" fill="#fff" stroke="none"/>
<circle cx="16.4" cy="18.3" r="1" fill="#fff" stroke="none"/>
</svg>`;
/* Kartenmarkierung: poi-car aus dem Audi-CI-Satz (assets/icons/poi-car.svg).
Die Spitze sitzt unten, deshalb ankert Leaflet am unteren Rand. */
function fahrzeugMarkerSVG(px = 34) {
return `<svg viewBox="0 0 24 24" width="${px}" height="${px}" fill="none" aria-hidden="true">${CI.poiCar}</svg>`;
}
/* Fahrzustand fürs Menü: "fährt" solange die aktuelle Fahrt noch offen ist
@@ -537,15 +539,22 @@ function distanzMeter(a, b) {
}
function distanzText(m) { return m == null ? "" : m < 1000 ? Math.round(m) + " m" : de(m / 1000, 1) + " km"; }
/* Zeile unter dem Fahrzeugnamen: Abstand Nutzer -> Fahrzeug, sonst der Grund,
warum er noch fehlt. */
function standortAbstandText() {
if (!standortBekannt()) return "";
if (USER_POS) return distanzText(distanzMeter(USER_POS, { lat: CAR.standortLat, lon: CAR.standortLon })) + " entfernt";
return USER_POS_FEHLER || "Abstand wird ermittelt …";
}
function standortErfassen() {
if (!navigator.geolocation) { USER_POS_FEHLER = "Kein Zugriff auf den Standort dieses Geräts"; return; }
if (!navigator.geolocation) { USER_POS_FEHLER = "Kein Zugriff auf den Standort dieses Geräts"; standortAnsichtAktualisieren(); return; }
navigator.geolocation.getCurrentPosition(
(pos) => {
USER_POS = { lat: pos.coords.latitude, lon: pos.coords.longitude, genauigkeit: pos.coords.accuracy };
USER_POS_TS = Date.now(); USER_POS_FEHLER = null;
standortAnsichtAktualisieren();
},
(err) => { USER_POS_FEHLER = err.code === err.PERMISSION_DENIED ? "Standortzugriff verweigert" : "Standort nicht verfügbar"; },
(err) => { USER_POS_FEHLER = err.code === err.PERMISSION_DENIED ? "Standortzugriff verweigert" : "Standort nicht verfügbar"; standortAnsichtAktualisieren(); },
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 30000 }
);
}
@@ -580,7 +589,7 @@ function standortAdresseLaden() {
if (!standortBekannt()) return;
standortAdresseAufloesen(CAR.standortLat, CAR.standortLon).then((adresse) => {
const el = $("#standortAdresse");
if (el) el.textContent = adresse || "Adresse nicht ermittelbar";
if (el && adresse) el.textContent = adresse;
});
}
@@ -605,22 +614,22 @@ function standortAnsichtAktualisieren() {
if (route.name !== "standort") return;
standortMarkerZeichnen();
const dist = $("#standortDistanz");
if (dist) dist.textContent = USER_POS && standortBekannt() ? distanzText(distanzMeter(USER_POS, { lat: CAR.standortLat, lon: CAR.standortLon })) : "";
if (dist) dist.textContent = standortAbstandText();
}
function standortTiles() {
if (!SMAP || !window.L) return;
const stil = SMAP_STIL || HOST.dataset.theme;
const url = TILES[stil];
if (SMAP_TILE) SMAP.removeLayer(SMAP_TILE);
SMAP_TILE = window.L.tileLayer(url, { maxZoom: 19, attribution: ATTR }).addTo(SMAP);
SMAP_TILE = SMAP_STIL === "satellit"
? window.L.tileLayer(SAT_TILE_URL, { maxZoom: 19, attribution: SAT_ATTR }).addTo(SMAP)
: window.L.tileLayer(TILES[HOST.dataset.theme], { maxZoom: 19, attribution: ATTR }).addTo(SMAP);
}
function standortMarkerZeichnen() {
if (!SMAP || !window.L) return;
if (FAHRZEUG_MARKER) { SMAP.removeLayer(FAHRZEUG_MARKER); FAHRZEUG_MARKER = null; }
if (USER_MARKER) { SMAP.removeLayer(USER_MARKER); USER_MARKER = null; }
if (standortBekannt()) {
const icon = window.L.divIcon({ className: "fahrzeug-pin-wrap", html: `<div class="fahrzeug-pin">${fahrzeugMarkerSVG()}</div>`, iconSize: [34, 34], iconAnchor: [17, 17] });
const icon = window.L.divIcon({ className: "fahrzeug-pin-wrap", html: `<div class="fahrzeug-pin">${fahrzeugMarkerSVG(38)}</div>`, iconSize: [38, 38], iconAnchor: [18, 36] });
FAHRZEUG_MARKER = window.L.marker([CAR.standortLat, CAR.standortLon], { icon }).addTo(SMAP);
FAHRZEUG_MARKER.on("click", () => standortMenuOeffnen());
}
@@ -648,9 +657,13 @@ async function initStandortMap() {
if (!el || typeof window.L === "undefined") return;
SMAP = window.L.map(el, { zoomControl: false, attributionControl: false });
SMAP_TILE = null; standortTiles();
/* Erst Ansicht, dann Marker: Leaflet legt Marker-Symbole nur auf einer Karte
mit gesetztem Mittelpunkt an - vorher angehaengte Marker blieben unsichtbar. */
if (standortBekannt()) SMAP.setView([CAR.standortLat, CAR.standortLon], 15);
else if (USER_POS) SMAP.setView([USER_POS.lat, USER_POS.lon], 15);
else SMAP.setView(kartenMittelpunkt(), 11);
standortMarkerZeichnen();
standortBeideZeigen();
if (!standortBekannt() && !USER_POS) SMAP.setView(kartenMittelpunkt(), 11);
standortAdresseLaden();
standortErfassenFallsNoetig();
setTimeout(() => SMAP && SMAP.invalidateSize(), 60);
@@ -665,9 +678,9 @@ async function initVorschauMap() {
});
TMAP_TILE = window.L.tileLayer(TILES[HOST.dataset.theme], { maxZoom: 19, attribution: ATTR }).addTo(TMAP);
if (standortBekannt()) {
const icon = window.L.divIcon({ className: "fahrzeug-pin-wrap", html: `<div class="fahrzeug-pin">${fahrzeugMarkerSVG()}</div>`, iconSize: [30, 30], iconAnchor: [15, 15] });
window.L.marker([CAR.standortLat, CAR.standortLon], { icon }).addTo(TMAP);
TMAP.setView([CAR.standortLat, CAR.standortLon], 14);
const icon = window.L.divIcon({ className: "fahrzeug-pin-wrap", html: `<div class="fahrzeug-pin">${fahrzeugMarkerSVG(32)}</div>`, iconSize: [32, 32], iconAnchor: [15, 30] });
window.L.marker([CAR.standortLat, CAR.standortLon], { icon }).addTo(TMAP);
} else {
TMAP.setView(kartenMittelpunkt(), 11);
}
@@ -685,6 +698,12 @@ function standortMenuUmschalten() { STANDORTMENU_OFFEN ? standortMenuSchliessen(
anderer Auslöser), manipuliert das Menü-DOM direkt (siehe Kopfkommentar
Standort-Abschnitt). */
let smY0 = null, smOffenBeimStart = false;
/* Zugweg: alles ausser dem sichtbaren Anteil. Die Blatthoehe haengt am Inhalt,
deshalb wird sie beim Ziehen gemessen statt fest verdrahtet. */
function standortMenuHub() {
const el = standortMenuElement();
return el ? Math.max(0, el.offsetHeight - STANDORT_PEEK_PX) : 0;
}
function standortMenuVerdrahten() {
ROOT.addEventListener("pointerdown", (e) => {
if (route.name !== "standort" || !e.target.closest(".standortmenu-griffzone")) return;
@@ -696,8 +715,9 @@ function standortMenuVerdrahten() {
e.preventDefault();
const el = standortMenuElement(); if (!el) return;
const dy = e.clientY - smY0;
const basis = smOffenBeimStart ? 0 : STANDORT_PEEK_PX;
const versatz = Math.max(0, Math.min(STANDORT_PEEK_PX, basis + dy));
const hub = standortMenuHub();
const basis = smOffenBeimStart ? 0 : hub;
const versatz = Math.max(0, Math.min(hub, basis + dy));
el.style.transform = `translateY(${versatz}px)`;
}, { passive: false });
ROOT.addEventListener("pointerup", (e) => {
@@ -720,28 +740,48 @@ function standortKachel() {
</div>`;
}
/* Symbole aus dem Audi-CI-Satz (assets/icons/) - hier eingebettet, weil die
Panel-Datei ohne zusaetzliche Abrufe auskommen soll. */
const CI = {
mapLayer: '<path fill="currentColor" d="m11.7 19.4-8.97-5.66.54-.85 8.96 5.67c.17.1.37.1.54 0l8.96-5.67.54.85-8.97 5.66a1.5 1.5 0 0 1-1.6 0m.53-3.84c.17.1.37.1.54 0l8.3-5.25-8.3-5.23a.5.5 0 0 0-.54 0l-8.3 5.23zm-.53.84-9.64-6.09 9.64-6.08a1.5 1.5 0 0 1 1.6 0l9.64 6.08-9.64 6.1c-.49.3-1.11.3-1.6 0Z"/>',
carSearch: '<path fill="currentColor" d="M1 7.8a6.16 6.16 0 0 0 6.1 6.1h.4A5.7 5.7 0 0 0 13 7.91V7.9a6.1 6.1 0 0 0-5.91-6A5.93 5.93 0 0 0 1 7.8m-1 0C0 3.92 3.23.8 7.11.9a7.1 7.1 0 0 1 6.89 7c.06 2.33-1 4.44-2.73 5.71L14.8 19H20v1.5c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5v-7.8l-1.9-1.9-.04-.06a55 55 0 0 0-2.84-4.71c-1.42-.1-2.49-.17-3.2-.2l.04-1c.77.04 1.93.1 3.48.22l.24.02.14.2a55 55 0 0 1 2.99 4.93L24 12.3v8.2c0 .83-.67 1.5-1.5 1.5h-2a1.5 1.5 0 0 1-1.5-1.5V20h-3.55l1.67 2.54-.84.54L14.26 20 5 19.96v.54c0 .83-.67 1.5-1.5 1.5h-2A1.5 1.5 0 0 1 0 20.5v-7.08h1v7.08c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5v-1.54l9.6.02-3.17-4.83c-.87.45-1.85.72-2.92.75H7.1c-3.88 0-7-3.13-7.1-7.09zm5.47-2.75c1.86-.12 3.37-.2 4.52-.24l.03 1c-1.08.03-2.5.1-4.24.22L4.51 8c-.27.43-.6 1-1 1.7l-.87-.48c.4-.72.74-1.3 1.03-1.76q.6-.96 1.41-2.19l.14-.2z"/>',
gps: '<path fill="currentColor" d="M12.5 3.51V6.5h-1V3.51a8.46 8.46 0 0 0-7.99 7.99H6.5v1H3.51a8.46 8.46 0 0 0 7.99 7.99V17.5h1v2.99a8.46 8.46 0 0 0 7.99-7.99H17.5v-1h2.99a8.46 8.46 0 0 0-7.99-7.99m0-1a9.46 9.46 0 0 1 8.99 8.99h2.01v1h-2.01a9.46 9.46 0 0 1-8.99 8.99v2.01h-1v-2.01a9.46 9.46 0 0 1-8.99-8.99H.5v-1h2.01a9.46 9.46 0 0 1 8.99-8.99V.5h1z"/>',
poiCar: '<path fill="currentColor" d="M3.8 8.6C3.8 4.33 7.32.8 11.5.8s7.7 3.53 7.7 7.8c0 1.11-.5 2.58-1.17 4.1a48 48 0 0 1-2.5 4.7 90 90 0 0 1-3.54 5.47l-.06.09-.02.02V23l-.41-.29-.4.29h-.01l-.02-.03-.06-.09a70 70 0 0 1-1.05-1.53 90 90 0 0 1-2.49-3.93 48 48 0 0 1-2.5-4.72C4.29 11.18 3.8 9.71 3.8 8.6m7.7 14.1-.4.29.4.57.4-.57zm0-.87a88 88 0 0 0 3.17-4.94 47 47 0 0 0 2.45-4.6A10.3 10.3 0 0 0 18.2 8.6c0-3.73-3.08-6.8-6.7-6.8a6.82 6.82 0 0 0-6.7 6.8c0 .89.4 2.18 1.08 3.69.67 1.48 1.55 3.1 2.45 4.6a89 89 0 0 0 3.17 4.94M8.77 4h5.47L17 7.35V11a1 1 0 0 1-1 1h-1.5a1 1 0 0 1-1-1h-4a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7.32zm.46 1L7 7.68V8h2v1H7v2h1.5v-1h6v1H16V9h-2V8h2v-.29L13.76 5z"/>',
selectAll: '<path fill="currentColor" d="M2.8 2H5v1H3v2H2V2.8a.76.76 0 0 1 .43-.71.9.9 0 0 1 .35-.09zv.5zM9 3H7V2h2zm4 0h-2V2h2zm4 0h-2V2h2zm4 0h-2V2h2.2v.5V2h.02a1 1 0 0 1 .1.01c.07.01.16.03.25.08a.76.76 0 0 1 .43.71V5h-1zM6 6h12v12H6zm1 1v10h10V7zM3 7v2H2V7zm19 0v2h-1V7zM3 11v2H2v-2zm19 0v2h-1v-2zM3 15v2H2v-2zm19 0v2h-1v-2zM3 21v-2H2v2.2a.76.76 0 0 0 .43.71.9.9 0 0 0 .35.09h.01l.01-.5v.5H5v-1zm19-2v2.2a.76.76 0 0 1-.43.71.9.9 0 0 1-.35.09h-.01l-.01-.49V22H19v-1h2v-2zM9 22H7v-1h2zm4 0h-2v-1h2zm4 0h-2v-1h2z"/>',
planRoute: '<path fill="currentColor" d="m18.3 7-3.65-3.65.7-.7 4.86 4.85-4.86 4.85-.7-.7L18.29 8H9.5C8.67 8 8 8.67 8 9.5V21H7V9.5A2.5 2.5 0 0 1 9.5 7z"/>',
upload: '<path fill="currentColor" d="M13 20h-1V9.7l-3.65 3.65-.7-.7 4.85-4.86 4.85 4.86-.7.7L13 9.71zM6 5V4h13v1z"/>',
fuelRange: '<path fill="currentColor" d="M2 3.5A2.5 2.5 0 0 1 4.5 1h6A2.5 2.5 0 0 1 13 3.5V10h.5a2.5 2.5 0 0 1 2.5 2.5v5a1.5 1.5 0 0 0 3 0V8.07a16.4 16.4 0 0 0-2.89-5.76l.78-.62a17.4 17.4 0 0 1 3.1 6.17l.01.07v9.57a2.5 2.5 0 0 1-5 0v-5c0-.83-.67-1.5-1.5-1.5H13v7h-1V3.5c0-.83-.67-1.5-1.5-1.5h-6C3.67 2 3 2.67 3 3.5V22h4v1H1v-1h1zM4 10h7v1H4zm5 14v-5h1v2h13v-2h1v5h-1v-2H10v2z"/>',
parking: '<path fill="currentColor" d="M0 2.5A2.5 2.5 0 0 1 2.5 0H23v20.5a2.5 2.5 0 0 1-2.5 2.5H0zM2.5 1C1.67 1 1 1.67 1 2.5V22h19.5c.83 0 1.5-.67 1.5-1.5V1zm11.59 6.35C13.6 7 13.02 7 12.5 7H8v6h4.5c.52 0 1.11-.01 1.59-.35.44-.3.91-1 .91-2.65 0-1.66-.47-2.34-.91-2.65M8 14v3H7V6h5.53c.5 0 1.38 0 2.13.53C15.47 7.09 16 8.16 16 10s-.53 2.9-1.34 3.47a3.6 3.6 0 0 1-2.13.53z"/>',
};
function ciSVG(pfad, px = 16) {
return `<svg viewBox="0 0 24 24" width="${px}" height="${px}" fill="none" aria-hidden="true">${pfad}</svg>`;
}
function vStandort() {
const zustand = standortZustand(), bekannt = standortBekannt();
const distanz = USER_POS && bekannt ? distanzText(distanzMeter(USER_POS, { lat: CAR.standortLat, lon: CAR.standortLon })) : "";
const distanz = standortAbstandText();
const adresseKey = bekannt ? CAR.standortLat.toFixed(3) + "," + CAR.standortLon.toFixed(3) : null;
/* Solange die Adresse nicht aufgeloest ist, stehen die Koordinaten da -
kein Platzhaltertext, der haengen bleiben kann. */
const koordinaten = bekannt ? CAR.standortLat.toFixed(4) + ", " + CAR.standortLon.toFixed(4) : "";
const adresseText = !bekannt ? "Kein GPS-Signal vom Fahrzeug"
: STANDORT_ADRESSE_KEY === adresseKey ? (STANDORT_ADRESSE || "Adresse nicht ermittelbar") : "Adresse wird ermittelt …";
: STANDORT_ADRESSE_KEY === adresseKey ? (STANDORT_ADRESSE || koordinaten) : koordinaten;
const link = standortKartenlink();
return `
<div class="standort-vollbild">
<div id="smap" class="standort-karte"></div>
<div class="kartensteuerung">
<button type="button" data-standort-layer aria-label="Kartendarstellung wählen">
<svg viewBox="0 0 20 20" width="19" height="19" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 2 2 6l8 4 8-4-8-4Z"/><path d="m2 10 8 4 8-4"/><path d="m2 14 8 4 8-4"/></svg>
${ciSVG(CI.mapLayer, 22)}
</button>
<button type="button" data-standort-fahrzeug aria-label="Auf Fahrzeug zentrieren">
<svg viewBox="0 0 24 24" width="19" height="19" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">${fahrzeugGlyphPfade()}</svg>
<button type="button" data-standort-fahrzeug aria-label="Fahrzeug auf der Karte suchen">
${ciSVG(CI.carSearch, 22)}
</button>
<button type="button" data-standort-user aria-label="Auf eigenen Standort zentrieren">
<svg viewBox="0 0 20 20" width="19" height="19" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="10" cy="10" r="2.2" fill="currentColor" stroke="none"/><circle cx="10" cy="10" r="7" stroke-linecap="round"/></svg>
${ciSVG(CI.gps, 22)}
</button>
<button type="button" data-standort-beide aria-label="Fahrzeug und eigenen Standort gemeinsam zeigen">
<svg viewBox="0 0 20 20" width="19" height="19" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3h5v5H3z"/><path d="M12 12h5v5h-5z"/><path d="m8 8 4 4"/></svg>
${ciSVG(CI.selectAll, 22)}
</button>
</div>
<div class="standortmenu" id="standortmenu">
@@ -751,23 +791,27 @@ function vStandort() {
<svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M5 5l10 10M15 5 5 15"/></svg>
</button>
<div class="standortmenu-kopf">
<span class="standortmenu-distanz" id="standortDistanz">${distanz}</span>
<div class="standortmenu-name">${esc(CONFIG.fahrzeugtitel)}</div>
<div class="standortmenu-adresse" id="standortAdresse">${esc(adresseText)}</div>
<div class="standortmenu-distanz" id="standortDistanz">${distanz}</div>
</div>
</div>
<div class="standortmenu-adresse" id="standortAdresse">${esc(adresseText)}</div>
<div class="standortmenu-status">
${zustand.faehrt ? `<span class="dot"></span><span>Fahrzeug fährt</span>`
: zustand.parkplatz ? `<span class="dot neutral"></span><span>Letzter Parkplatz · geparkt seit ${dauerSeitText(zustand.seitTs)}</span>`
: `<span class="dot neutral"></span><span>Fahrzeug steht${zustand.seitTs != null ? " · seit " + dauerSeitText(zustand.seitTs) : ""}</span>`}
: `${ciSVG(CI.parking)}<span>${zustand.seitTs != null ? "Geparkt seit " + dauerSeitText(zustand.seitTs) : "Fahrzeug steht"}</span>`}
</div>
<div class="standortmenu-werte">
<div><span class="label">Tankfüllstand</span><div class="wert" style="margin-top:4px">${de(CAR.tankPct)} %</div></div>
<div><span class="label">Restreichweite</span><div class="wert" style="margin-top:4px">${CAR.reichweite != null ? de(CAR.reichweite) + " km" : "unbekannt"}</div></div>
${ciSVG(CI.fuelRange)}
<span>${de(CAR.tankPct)} %</span>
<span>${CAR.reichweite != null ? de(CAR.reichweite) + " km" : "Reichweite unbekannt"}</span>
</div>
<div class="standortmenu-aktionen">
<a class="aktion primaer" style="text-align:center;text-decoration:none;box-sizing:border-box${link ? "" : ";pointer-events:none;opacity:.4"}"${link ? ` href="${link}" target="_blank" rel="noopener"` : ` aria-disabled="true"`}>Route</a>
<button type="button" class="aktion" data-standort-teilen ${bekannt ? "" : "disabled"}>Teilen</button>
<a class="standort-pille primaer"${link ? ` href="${link}" target="_blank" rel="noopener"` : ` aria-disabled="true" style="pointer-events:none;opacity:.4"`}>
${ciSVG(CI.planRoute, 15)}Route
</a>
<button type="button" class="standort-pille" data-standort-teilen ${bekannt ? "" : "disabled"}>
${ciSVG(CI.upload, 15)}Teilen
</button>
</div>
</div>
</div>`;
@@ -947,16 +991,6 @@ function vHome() {
<div class="balken"><i style="width:${pct}%;background:${balkenFarbe}"></i></div>
<span class="label" style="margin-top:12px">Reichweite${liter != null ? ` · noch etwa ${de(liter)} l im Tank` : ""}</span>
</div>
<div class="duo">
<button class="tile tilebtn" data-go="sicherheit:x"><span class="label">Fahrzeugstatus</span>
<div class="wert" style="margin-top:10px;color:var(--fg);font-size:15px">${g === null ? "unbekannt" : g ? "gesichert" : "prüfen"}</div></button>
<button class="tile tilebtn" data-go="service:x"><span class="label">Service</span>
<div class="wert" style="margin-top:10px;color:var(--fg);font-size:15px">${t ? esc(t.art) : "kein Eintrag"}</div></button>
<button class="tile tilebtn" data-go="reifen:x"><span class="label">Reifen</span>
<div class="wert" style="margin-top:10px;color:var(--fg);font-size:15px">${esc(CAR.reifen.aktiv)}</div></button>
<button class="tile tilebtn" data-go="fuel:x"><span class="label">Tanken</span>
<div class="wert" style="margin-top:10px;color:var(--fg);font-size:15px">${FILLS.length ? de(FILLS.length) + " Vorgänge" : "noch keiner"}</div></button>
</div>
<div class="duo">
<div class="tile"><span class="label">Kilometerstand</span>
<div class="fig" style="font-size:29px;margin-top:14px">${CAR.odoBekannt ? de(CAR.odo) : ""}<span class="unit">km</span></div></div>
@@ -2439,7 +2473,9 @@ function vFill(id) {
/* ---------------------------------------------------------- Steuerung */
const ICONS = {
home: '<path d="M4 17a9 9 0 0 1 18 0"/><path d="M13 17l5.5-5"/><circle cx="13" cy="17" r="1.3"/>',
audi: '<polygon points="21.6,5 16.5,19 2.4,19 7.5,5" transform="translate(0,0)"/>',
/* Wagen von vorn - dasselbe Symbol wie in der myAudi-App (Audi-Symbolsatz,
assets/icons/car.svg). Flaeche statt Kontur, deshalb die Klasse "voll". */
audi: '<path d="M5.46 4.05a80 80 0 0 1 13.08 0l.24.02.14.2A55 55 0 0 1 21.9 9.2l2.1 2.1v8.2c0 .83-.67 1.5-1.5 1.5h-2a1.5 1.5 0 0 1-1.5-1.5V19l-14-.04v.54c0 .83-.67 1.5-1.5 1.5h-2A1.5 1.5 0 0 1 0 19.5v-8.2l2.1-2.1a55 55 0 0 1 2.98-4.93l.14-.2zM2.7 10 1 11.7v.3h4v1H1v6.5c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5v-1.55L20 18v1.5c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5V13h-4v-1h4v-.3L21.3 10zm17.94-1a45 45 0 0 0-2.43-3.98 78 78 0 0 0-12.44 0L4.51 7c-.3.5-.7 1.16-1.16 2.01h17.3ZM7.3 12h9.42l1.36 1.36-.1.3c-.31 1-.6 1.78-.88 2.31-.13.27-.27.5-.41.67-.13.15-.36.36-.68.36H8a.9.9 0 0 1-.68-.36c-.14-.17-.28-.4-.41-.67a17 17 0 0 1-.89-2.32l-.09-.29zm.42 1-.64.64c.28.84.52 1.47.73 1.89a2.4 2.4 0 0 0 .3.47h7.8l.02-.01c.07-.08.16-.23.28-.46.21-.42.45-1.05.73-1.9L16.3 13H7.71Z"/>',
trips: '<circle cx="6" cy="6" r="2.2"/><circle cx="18" cy="18" r="2.2"/><path d="M8 7.5c4 1.5 4.5 3 3 5s-1 3.5 4.5 5"/>',
stat: '<path d="M4 20V9.5M10 20V4.5M16 20v-8M22 20H2"/>',
fuel: '<path d="M4.5 20V6.2c0-.9.8-1.7 1.7-1.7h5.6c1 0 1.7.8 1.7 1.7V20"/><path d="M3 20h12.5"/><path d="M4.5 11h10.5"/><path d="M17 20v-8.5a2 2 0 0 0-2-2"/><path d="M17 11.5V7.8l-2-2.3"/>',
@@ -2451,6 +2487,8 @@ const TABS = [
{ id: "stat", label: "Statistik", eyebrow: "", title: "Statistik" },
{ id: "fuel", label: "Tanken", eyebrow: "", title: "Tanken" },
];
/* Symbole, die als Flaeche gezeichnet sind statt als Kontur */
const ICONS_VOLL = new Set(["audi"]);
let route = { name: "home" };
const ZURUECK = {
trip: "trips", fill: "fuel", service: "audi", reifen: "audi", ident: "audi",
@@ -2512,6 +2550,14 @@ let letzteAnsicht = null;
let routeStack = [];
let navRichtung = null;
const istTab = (name) => TABS.some((t) => t.id === name);
/* Zu welchem Tab gehoert eine Unterseite? Erst der tatsaechlich gelaufene Weg
(routeStack), sonst die ZURUECK-Tabelle. */
function tabVon(name) {
if (routeStack.length && istTab(routeStack[0].name)) return routeStack[0].name;
let n = name, i = 0;
while (n && !istTab(n) && i++ < 8) n = ZURUECK[n];
return istTab(n) ? n : null;
}
function render() {
if (!DATEN_GELADEN) return;
// Eine laufende Wischgeste bezieht sich auf Knoten, die render() gleich
@@ -2565,7 +2611,15 @@ function render() {
if (route.name === "standort") initStandortMap().catch(() => {});
if (TMAP) { TMAP.remove(); TMAP = null; TMAP_TILE = null; }
if (route.name === "home") initVorschauMap().catch(() => {});
ROOT.querySelectorAll(".tab").forEach((el) => el.classList.toggle("on", el.dataset.tab === route.name));
/* Menueleiste wie in der myAudi-App: genau ein Eintrag traegt die gefuellte
Pille, und zwar auch dann, wenn eine Unterseite offen ist - die gehoert
zum Tab, aus dem sie aufgerufen wurde (ZURUECK zeigt darauf). */
const aktiverTab = istTab(route.name) ? route.name : tabVon(route.name);
ROOT.querySelectorAll(".tab").forEach((el) => {
const an = el.dataset.tab === aktiverTab;
el.classList.toggle("on", an);
el.setAttribute("aria-current", an ? "page" : "false");
});
}
const go = (name, id) => {
// Ein unbestätigter Servicebuch-Entwurf (siehe data-sbneu) verlässt die
@@ -2618,13 +2672,30 @@ function serviceRufen(dienst, daten) {
function ereignisseVerdrahten() {
ROOT.getElementById("tabbar").innerHTML = TABS.map((t) =>
`<button class="tab" data-tab="${t.id}">
<svg viewBox="0 0 24 24" width="21" height="21" aria-hidden="true">${ICONS[t.id]}</svg><span>${t.label}</span>
`<button class="tab" data-tab="${t.id}" aria-current="false">
<span class="tabpille"><svg viewBox="0 0 24 24" width="21" height="21" aria-hidden="true"${ICONS_VOLL.has(t.id) ? ' class="voll"' : ""}>${ICONS[t.id]}</svg></span><span>${t.label}</span>
</button>`).join("");
ROOT.getElementById("tabbar").classList.toggle("ohne", !beschriftung);
/* Auf breiten Bildschirmen steht die Menueleiste als Spalte links; die Marke
gehoert dann an deren Kopf statt in die Kopfzeile (Design-Audit). Die Ringe
werden aus der Kopfzeile geklont, die Ids im SVG umbenannt, damit der
clipPath eindeutig bleibt. Sichtbar nur ueber die Regel in
audi-dashboard-ios.css - in Telefonbreite bleibt der Klon verborgen. */
const tabbar = ROOT.getElementById("tabbar");
if (!tabbar.querySelector(".navmarke")) {
const marke = document.createElement("div");
marke.className = "navmarke";
marke.setAttribute("aria-hidden", "true");
marke.innerHTML = ROOT.getElementById("marke").innerHTML
.replace(/audiRingsClip/g, "audiRingsClipNav");
tabbar.prepend(marke);
}
ROOT.getElementById("tabbar").addEventListener("click", (e) => { const b = e.target.closest(".tab"); if (b) go(b.dataset.tab); });
ROOT.getElementById("back").addEventListener("click", () => zurueck());
ROOT.getElementById("ringsbtn").addEventListener("click", () => go("einst"));
/* In Telefonbreite sind die Ringe oben rechts der Zugang zu den
Einstellungen (das Zahnrad ist dort ausgeblendet). */
ROOT.getElementById("marke").addEventListener("click", () => go("einst"));
ROOT.getElementById("theme").addEventListener("click", () => {
themeSetzen(HOST.dataset.theme === "nacht" ? "tag" : "nacht");
themeIcon();
@@ -3073,7 +3144,7 @@ function ereignisseVerdrahten() {
if (art) { const tr = TRIPS.find((x) => x.trip_id === art.dataset.art); tr.art = tr.art === "arbeitsweg" ? "privat" : "arbeitsweg"; render(); return; }
// Standort (Vollbild) - Karten-/Menü-Bedienung manipuliert SMAP/das Menü-
// DOM direkt statt über render() (siehe Kopfkommentar Standort-Abschnitt).
if (e.target.closest("[data-standort-layer]")) { SMAP_STIL = (SMAP_STIL || HOST.dataset.theme) === "nacht" ? "tag" : "nacht"; standortTiles(); return; }
if (e.target.closest("[data-standort-layer]")) { SMAP_STIL = SMAP_STIL === "satellit" ? "strasse" : "satellit"; standortTiles(); return; }
if (e.target.closest("[data-standort-fahrzeug]")) { standortAufFahrzeug(); return; }
if (e.target.closest("[data-standort-user]")) { standortAufUser(); return; }
if (e.target.closest("[data-standort-beide]")) { standortBeideZeigen(); return; }