From 7e979ecfab61963be8e9ca47c328e236ba302c5b Mon Sep 17 00:00:00 2001 From: Tobi G Date: Wed, 12 Aug 2026 12:32:34 +0200 Subject: [PATCH] =?UTF-8?q?Standort:=20Feinschliff=20aus=20Claude=20Design?= =?UTF-8?q?=20=C3=BCbernehmen=20+=20Satellit/Stra=C3=9Fenkarte-Umschalter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ü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. --- homeassistant/www/audi-dashboard-app.js | 177 +++++++++++++++++------- 1 file changed, 124 insertions(+), 53 deletions(-) diff --git a/homeassistant/www/audi-dashboard-app.js b/homeassistant/www/audi-dashboard-app.js index c6be1b7..3be45a8 100644 --- a/homeassistant/www/audi-dashboard-app.js +++ b/homeassistant/www/audi-dashboard-app.js @@ -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 ` `; } -function fahrzeugMarkerSVG() { - return ` - ${fahrzeugGlyphPfade()} - - - `; +/* 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 ``; } /* 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: `
${fahrzeugMarkerSVG()}
`, iconSize: [34, 34], iconAnchor: [17, 17] }); + const icon = window.L.divIcon({ className: "fahrzeug-pin-wrap", html: `
${fahrzeugMarkerSVG(38)}
`, 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: `
${fahrzeugMarkerSVG()}
`, 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: `
${fahrzeugMarkerSVG(32)}
`, 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() { `; } +/* Symbole aus dem Audi-CI-Satz (assets/icons/) - hier eingebettet, weil die + Panel-Datei ohne zusaetzliche Abrufe auskommen soll. */ +const CI = { + mapLayer: '', + carSearch: '', + gps: '', + poiCar: '', + selectAll: '', + planRoute: '', + upload: '', + fuelRange: '', + parking: '', +}; +function ciSVG(pfad, px = 16) { + return ``; +} + 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 `
-
@@ -751,23 +791,27 @@ function vStandort() {
- ${distanz}
${esc(CONFIG.fahrzeugtitel)}
-
${esc(adresseText)}
+
${distanz}
+
${esc(adresseText)}
${zustand.faehrt ? `Fahrzeug fährt` - : zustand.parkplatz ? `Letzter Parkplatz · geparkt seit ${dauerSeitText(zustand.seitTs)}` - : `Fahrzeug steht${zustand.seitTs != null ? " · seit " + dauerSeitText(zustand.seitTs) : ""}`} + : `${ciSVG(CI.parking)}${zustand.seitTs != null ? "Geparkt seit " + dauerSeitText(zustand.seitTs) : "Fahrzeug steht"}`}
-
Tankfüllstand
${de(CAR.tankPct)} %
-
Restreichweite
${CAR.reichweite != null ? de(CAR.reichweite) + " km" : "unbekannt"}
+ ${ciSVG(CI.fuelRange)} + ${de(CAR.tankPct)} % + ${CAR.reichweite != null ? de(CAR.reichweite) + " km" : "Reichweite unbekannt"}
- Route - + + ${ciSVG(CI.planRoute, 15)}Route + +
`; @@ -947,16 +991,6 @@ function vHome() {
Reichweite${liter != null ? ` · noch etwa ${de(liter)} l im Tank` : ""} -
- - - - -
Kilometerstand
${CAR.odoBekannt ? de(CAR.odo) : "–"}km
@@ -2439,7 +2473,9 @@ function vFill(id) { /* ---------------------------------------------------------- Steuerung */ const ICONS = { home: '', - audi: '', + /* Wagen von vorn - dasselbe Symbol wie in der myAudi-App (Audi-Symbolsatz, + assets/icons/car.svg). Flaeche statt Kontur, deshalb die Klasse "voll". */ + audi: '', trips: '', stat: '', fuel: '', @@ -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) => - ``).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; }