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 ``;
+/* 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"}
`;
@@ -947,16 +991,6 @@ function vHome() {
Reichweite${liter != null ? ` · noch etwa ${de(liter)} l im Tank` : ""}
-
-
Fahrzeugstatus
- ${g === null ? "unbekannt" : g ? "gesichert" : "prüfen"}
-
Service
- ${t ? esc(t.art) : "kein Eintrag"}
-
Reifen
- ${esc(CAR.reifen.aktiv)}
-
Tanken
- ${FILLS.length ? de(FILLS.length) + " Vorgänge" : "noch keiner"}
-
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) =>
- `
- ${t.label}
+ `
+ ${t.label}
`).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; }