Ortsnamen aus dem Backend, Verbrauchsfaktor aus dem Tankbeleg, Radzaehler ueber Fahrten
Ortsauflösung serverseitig (geokodierung.py): der Ort steht jetzt in der Fahrt statt im Zwischenspeicher jedes Geräts. Beide Oberflächen fragen nichts mehr ab. Verbrauchskorrektur (verbrauchskorrektur.py): Faktor = Beleg-Liter / Summe der Einzelfahrten, voll zu voll. Statt einer Schranke am Faktor wird der Nenner geprüft - decken die erkannten Fahrten 90-110 % der Tacho-Spanne ab? Ausgelöst beim Schreiben eines Tankvorgangs, nicht erst bei der nächsten Fahrt. Radzähler: summiert die Fahrten des montierten Satzes statt Tacho-Deltas. Der alte Weg hatte Fahrzeugwechsel als gefahrene Strecke verbucht (375.962 km bei einem Tacho von 61.823). Null heißt unbekannt: ein Kilometerstand von 0 oder ein leeres Feld werden zu None normalisiert, statt an sechs Lesestellen als "Tachostand null" zu gelten. Momentanwerte der Sensoren gelten nur für einen Tankvorgang von jetzt. Oberfläche, beide Codebasen: Wertespalte der Fahrtenliste ausgerichtet, Ort -> Ort in "Zuletzt", "Räder" statt "Reifen", "Termin vereinbart" entfernt, Markenlogo 20 % größer, Übersicht-Symbol auf volle Rasterbreite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -754,7 +754,6 @@ let USER_POS_FEHLER = null;
|
||||
let USER_WATCH = null; // watchPosition-Kennung, solange die Standortseite offen ist
|
||||
let STANDORT_ADRESSE = null; // zuletzt aufgelöste Adresse (Cache über Koordinaten-Rundung)
|
||||
let STANDORT_ADRESSE_KEY = null;
|
||||
let STANDORT_STADT = ""; // reiner Ortsname zum selben Punkt
|
||||
let STANDORTMENU_OFFEN = false;
|
||||
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
|
||||
@@ -956,34 +955,19 @@ function standortVerfolgungBeenden() {
|
||||
der Einzelfahrt bisher "unbekannt", obwohl die Position bekannt war
|
||||
(vom Eigentümer gemeldet, 01.09.2026).
|
||||
|
||||
Aufgelöst wird erst beim Öffnen der Fahrt und über denselben Cache wie die
|
||||
Standortansicht - ein Abruf je Ort, danach nie wieder. Ein Vorratsabruf für
|
||||
alle Fahrten der Liste wäre genau das, worum Nominatim in seinen
|
||||
Nutzungsbedingungen bittet, es nicht zu tun. */
|
||||
Aufgelöst wird seit dem 01.09.2026 im Backend (geokodierung.py) und mit der
|
||||
Fahrt gespeichert. Der Client fragt nichts mehr ab und hält keinen eigenen
|
||||
Zwischenspeicher: jedes Gerät zeigt denselben Ort, ein frisch eingerichtetes
|
||||
Telefon zeigt ihn sofort, und ein geleerter Browserspeicher wirft nichts weg.
|
||||
|
||||
"wird ermittelt …" heißt: die Koordinaten stehen, der Ort noch nicht. Das
|
||||
nächste Screening trägt ihn nach. */
|
||||
function fahrtOrtText(adresse, lat, lon) {
|
||||
if (adresse) return esc(adresse);
|
||||
if (lat == null || lon == null) return "unbekannt";
|
||||
return "wird ermittelt …";
|
||||
}
|
||||
|
||||
/* Trägt die aufgelösten Orte nach dem Zeichnen ein. Die Fahrt-Kennung wird
|
||||
mitgeführt: wer während des Abrufs weiterblättert, soll nicht die Adresse
|
||||
der vorigen Fahrt in der neuen sehen. */
|
||||
async function fahrtOrteNachtragen(t) {
|
||||
const id = t.trip_id;
|
||||
const paare = [
|
||||
["start", t.start_address, t.start_lat, t.start_lon],
|
||||
["ziel", t.end_address, t.end_lat, t.end_lon],
|
||||
];
|
||||
for (const [feld, adresse, lat, lon] of paare) {
|
||||
if (adresse || lat == null || lon == null) continue;
|
||||
const ort = await standortAdresseAufloesen(lat, lon);
|
||||
const ziel = ROOT.querySelector(`[data-fahrtort="${feld}"]`);
|
||||
if (!ziel || route.name !== "trip" || route.id !== id) return;
|
||||
ziel.textContent = ort || "unbekannt";
|
||||
}
|
||||
}
|
||||
|
||||
/* Reverse-Geocoding über die öffentliche Nominatim-API (OpenStreetMap) -
|
||||
wie die Kartenkacheln oben (TILE_URL) ohne eigenen Schlüssel; bei diesem
|
||||
Aufrufvolumen (ein Fahrzeug, nur bei geöffneter Standortansicht) im Rahmen
|
||||
@@ -1017,8 +1001,7 @@ async function standortAdresseAufloesen(lat, lon) {
|
||||
const daten = await res.json();
|
||||
const a = daten.address || {};
|
||||
const strasse = [a.road, a.house_number].filter(Boolean).join(" ");
|
||||
STANDORT_STADT = (a.city || a.town || a.village || "").trim();
|
||||
const ort = [a.postcode, STANDORT_STADT].filter(Boolean).join(" ");
|
||||
const ort = [a.postcode, a.city || a.town || a.village].filter(Boolean).join(" ");
|
||||
/* Steht das Auto auf einem benannten Platz - Parkplatz, Parkhaus,
|
||||
Rastplatz -, sagt dessen Name mehr als die naechstgelegene Hausnummer.
|
||||
Nominatim liefert ihn im Feld name; die Pruefung auf die Art der
|
||||
@@ -1040,9 +1023,6 @@ async function standortAdresseAufloesen(lat, lon) {
|
||||
// Nur echte Treffer merken - eine gedrosselte Antwort ist kein "hier gibt es
|
||||
// keine Adresse" und soll beim naechsten Mal erneut versucht werden.
|
||||
if (STANDORT_ADRESSE) geoCacheSchreiben(cacheKey, STANDORT_ADRESSE);
|
||||
// Der reine Ortsname zusaetzlich, fuer die Fahrtenliste - siehe
|
||||
// stadtAusCache().
|
||||
if (STANDORT_STADT) geoCacheSchreiben("ort:" + key, STANDORT_STADT);
|
||||
return STANDORT_ADRESSE;
|
||||
}
|
||||
function standortAdresseLaden() {
|
||||
@@ -1502,7 +1482,7 @@ function teaser() {
|
||||
const t = juengster(TRIPS, "ts_start"), f = juengster(FILLS, "ts");
|
||||
return `<div class="tile teaser"><span class="label">Zuletzt</span>
|
||||
${t ? `<button class="leaf" style="padding-left:0" data-go="trip:${t.trip_id}">
|
||||
<span class="k">Letzte Fahrt<small>${esc(t.start_address || "")} → ${esc(t.end_address || "")}</small></span>
|
||||
<span class="k">Letzte Fahrt<small>${strecke(t)}</small></span>
|
||||
<span class="v">${streckeText(t.distance_km, "—")}<small>${fahrtVerbrauch(t) != null ? de(fahrtVerbrauch(t), 1) + " l/100 km" : "—"}</small></span>
|
||||
<svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></button>` : ""}
|
||||
${f ? `<button class="leaf" style="padding-left:0;border-bottom:0" data-go="fill:${f.tank_id}">
|
||||
@@ -1973,13 +1953,13 @@ function vAudi() {
|
||||
<dd>${eur(CAR.versicherung.beitrag + CAR.steuer.betrag)} €</dd></div>
|
||||
</dl></button>
|
||||
<button class="tile tilebtn" data-go="reifen:x">
|
||||
<span class="label">Reifen</span>
|
||||
<span class="label">Räder</span>
|
||||
<svg class="go" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg>
|
||||
<dl class="rows" style="margin-top:10px">
|
||||
<div class="row"><dt>aktuell montiert:</dt><dd>${r.aktiv}räder</dd></div>
|
||||
<div class="row"><dt>Laufleistung</dt><dd>${a.km != null ? de(a.km) + " km" : "unbekannt"}</dd></div>
|
||||
<div class="row" style="border-bottom:0"><dt>Wechseltermin</dt>
|
||||
<dd>${dedat(new Date(r.wechsel.datum))}${r.wechsel.vereinbart ? "<small>vereinbart</small>" : ""}</dd></div>
|
||||
<dd>${dedat(new Date(r.wechsel.datum))}${r.wechsel.vereinbart ? "" : ""}</dd></div>
|
||||
</dl></button>`;
|
||||
}
|
||||
|
||||
@@ -2971,7 +2951,7 @@ function vEinst() {
|
||||
</div>
|
||||
<div class="tile"><span class="label">Kraftstoff-Rabatt</span>
|
||||
<div class="feld"><label for="sdAktiv" style="display:flex;align-items:center;gap:6px">
|
||||
<img src="${STATIK}/bilder/${MARKENLOGO_DATEI}" alt="Shell" style="height:16px;width:auto" onerror="this.style.display='none'"> SmartDeal aktiv</label>
|
||||
<img src="${STATIK}/bilder/${MARKENLOGO_DATEI}" alt="Shell" style="height:19.2px;width:auto" onerror="this.style.display='none'"> SmartDeal aktiv</label>
|
||||
<label class="switch"><input id="sdAktiv" type="checkbox" data-sd ${CAR.smartdeal.aktiv ? "checked" : ""}><span class="track"></span></label></div>
|
||||
${CAR.smartdeal.aktiv ? `
|
||||
<div class="feld" style="border-bottom:0"><label>Gültig bis</label>
|
||||
@@ -2992,7 +2972,7 @@ function vEinst() {
|
||||
<span class="label" style="margin-top:10px;color:var(--fg2);letter-spacing:0;
|
||||
text-transform:none;font-size:12.5px;line-height:1.6">
|
||||
Alles Fahrzeugspezifische liegt in einer einzigen Datei: Stammdaten, technische Daten,
|
||||
Ausstattung, Reifen, Versicherung, Steuer und Werkstatt. Fahrten, Tankvorgänge und der
|
||||
Ausstattung, Räder, Versicherung, Steuer und Werkstatt. Fahrten, Tankvorgänge und der
|
||||
Wartungsplan lassen sich einzeln als CSV sichern und bearbeitet wieder einspielen — eine
|
||||
Zeile mit bekannter ID aktualisiert nur ihre eigenen Spalten, alles andere bleibt
|
||||
unangetastet.</span>
|
||||
@@ -3432,10 +3412,8 @@ function vReifen() {
|
||||
</div>` : ""}
|
||||
</div>
|
||||
<div class="tile"><span class="label">Wechseltermin</span>
|
||||
<div class="feld"><label for="rfDat">Geplant am</label>
|
||||
<div class="feld" style="border-bottom:0"><label for="rfDat">Geplant am</label>
|
||||
<input id="rfDat" type="date" value="${r.wechsel.datum}"></div>
|
||||
<div class="feld" style="border-bottom:0"><label for="rfOk">Termin vereinbart</label>
|
||||
<label class="switch"><input id="rfOk" type="checkbox" ${r.wechsel.vereinbart ? "checked" : ""}><span class="track"></span></label></div>
|
||||
<button class="aktion" data-ics="reifen">In den Kalender übernehmen</button>
|
||||
</div>
|
||||
${satz("sommer")}
|
||||
@@ -3725,74 +3703,20 @@ function istNachtZeit(ts) {
|
||||
}
|
||||
/* D3: Die Strecke führt die Zeile, das Datum steht darunter. Umbruch nur
|
||||
zwischen den Orten, nie am Pfeil. */
|
||||
/* Ein bereits aufgeloester Ort aus dem Zwischenspeicher - OHNE Netzabruf.
|
||||
|
||||
Die Einzelfahrt loest Koordinaten bei Bedarf auf und legt das Ergebnis im
|
||||
selben Fach ab wie die Standortansicht. Was einmal dort steht, kann die
|
||||
Liste umsonst mitbenutzen: kein zusaetzlicher Abruf, keine Wartezeit, kein
|
||||
Flackern beim Zeichnen.
|
||||
|
||||
Bewusst NUR aus dem Speicher. Ein Abruf je Zeile waere genau der
|
||||
Vorratsabruf, um dessen Unterlassung Nominatim in seinen
|
||||
Nutzungsbedingungen bittet - bei zweihundert Fahrten waeren das
|
||||
vierhundert Anfragen fuer einen Blick auf die Liste.
|
||||
|
||||
Schluessel wie bei der Aufloesung: drei Nachkommastellen (~110 m). */
|
||||
function ortAusCache(lat, lon) {
|
||||
if (lat == null || lon == null) return null;
|
||||
const wert = geoCacheLesen()["rev:" + lat.toFixed(3) + "," + lon.toFixed(3)];
|
||||
return typeof wert === "string" ? wert : null;
|
||||
}
|
||||
|
||||
/* Der Ortsname allein, fuer die Fahrtenliste.
|
||||
|
||||
In der Liste steht eine Fahrt in einer Zeile neben Datum, Art, Strecke und
|
||||
Verbrauch. Zwei volle Anschriften passen dort nicht - "Schottenau, 85072
|
||||
Eichstaett -> Am Anger 7a, 85111 Adelschlag" ist laenger als der Rest der
|
||||
Zeile zusammen. Auf der Einzelfahrt bleibt die vollstaendige Anschrift.
|
||||
|
||||
Der Ort wird beim Aufloesen SEPARAT gemerkt (Schluessel "ort:"), nicht
|
||||
nachtraeglich aus der Anschrift geschnitten. Das Zerlegen der eigenen
|
||||
Ausgabe waere fast immer richtig und in einem Fall falsch: faellt Nominatim
|
||||
auf display_name zurueck, steht hinter dem letzten Komma das LAND. Dann
|
||||
stuende in der Liste "Deutschland -> Deutschland". */
|
||||
/* Der Ortsname aus einer bereits gespeicherten Anschrift.
|
||||
|
||||
Rueckfall fuer alles, was VOR dem Merken des Ortsnamens aufgeloest wurde:
|
||||
dort liegt nur die volle Anschrift, und ein neuer Netzabruf, nur um den Ort
|
||||
zu erfahren, waere Verschwendung.
|
||||
|
||||
Geschnitten wird ausschliesslich, wenn hinter dem letzten Komma eine
|
||||
Postleitzahl steht - unser eigenes Format ist "<Strasse>, <PLZ> <Ort>".
|
||||
Faellt Nominatim auf display_name zurueck, fehlt die PLZ und hinter dem
|
||||
letzten Komma stuende das LAND; dann lieber die volle Anschrift zeigen als
|
||||
"Deutschland". */
|
||||
function stadtAusAnschrift(anschrift) {
|
||||
if (!anschrift) return null;
|
||||
const teil = String(anschrift).split(", ").pop().trim();
|
||||
const treffer = teil.match(/^\d{4,5}\s+(.+)$/);
|
||||
return treffer ? treffer[1].trim() : null;
|
||||
}
|
||||
|
||||
function stadtAusCache(lat, lon) {
|
||||
if (lat == null || lon == null) return null;
|
||||
const wert = geoCacheLesen()["ort:" + lat.toFixed(3) + "," + lon.toFixed(3)];
|
||||
return typeof wert === "string" && wert ? wert : null;
|
||||
}
|
||||
|
||||
function strecke(t) {
|
||||
// Ortsname bevorzugt; ohne ihn die volle Anschrift, damit eine vor dieser
|
||||
// Aenderung aufgeloeste Fahrt nicht ploetzlich wieder ortlos dasteht.
|
||||
const ortsname = (adresse, lat, lon) => {
|
||||
if (adresse) return adresse;
|
||||
const gemerkt = stadtAusCache(lat, lon);
|
||||
if (gemerkt) return gemerkt;
|
||||
const voll = ortAusCache(lat, lon);
|
||||
return stadtAusAnschrift(voll) || voll;
|
||||
// Der Ortsname kommt mit der Fahrt (start_stadt), aufgeloest im Backend.
|
||||
//
|
||||
// Eine VON HAND eingetragene Anschrift sticht ihn: wer "Zuhause" eintraegt,
|
||||
// will das auch in der Liste lesen und nicht den Ortsnamen dazu. Erkennbar
|
||||
// an edited_fields - genau dafuer fuehrt das Backend die Liste.
|
||||
const handisch = new Set(t.edited_fields || []);
|
||||
const ortsname = (seite) => {
|
||||
const adresse = t[seite + "_address"];
|
||||
if (adresse && handisch.has(seite + "_address")) return adresse;
|
||||
return t[seite + "_stadt"] || adresse || null;
|
||||
};
|
||||
const a = ortsname(t.start_address, t.start_lat, t.start_lon);
|
||||
const b = ortsname(t.end_address, t.end_lat, t.end_lon);
|
||||
if (!a && !b) return "Fahrt ohne Ortsangabe";
|
||||
const a = ortsname("start");
|
||||
const b = ortsname("end"); if (!a && !b) return "Fahrt ohne Ortsangabe";
|
||||
if (!b) return esc(a);
|
||||
if (!a) return esc(b);
|
||||
return `${esc(a)} → ${esc(b)}`;
|
||||
@@ -4253,7 +4177,21 @@ 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"/>',
|
||||
/* Auf die volle Rasterbreite gezogen. Live gemessen (getBBox, 01.09./02.09.2026)
|
||||
fuellte das Symbol nur 18,0 x 10,3 von 24 - 43 % der Hoehe, waehrend die
|
||||
vier Nachbarn zwischen 65 % (Tanken) und 89 % (Fahrten) liegen. In einer
|
||||
Reihe liest sich das als "zu klein", und genau so wurde es gemeldet.
|
||||
|
||||
Es ist bewusst NUR ein Faktor 1,33 (18 -> 24 Breite, Hoehe 10,3 -> 13,7)
|
||||
und keine Angleichung auf 72 %: das Symbol ist ein Halbkreis, also
|
||||
bauartbedingt halb so hoch wie breit. Fuer 72 % Hoehe braeuchte es Faktor
|
||||
1,68 und waere mit 30 Einheiten breiter als das Raster. Wer es wirklich
|
||||
gleich hoch will, muss den Bogen neu zeichnen (groesserer Winkel als 180
|
||||
Grad), nicht skalieren.
|
||||
|
||||
stroke-width am <g> gleicht die Skalierung aus: 1,5 / 1,33 = 1,125, damit
|
||||
die Strichstaerke gerendert wieder 1,5 betraegt wie bei den Nachbarn. */
|
||||
home: '<g transform="translate(-1.33,-2.67) scale(1.33)" stroke-width="1.125"><path d="M4 17a9 9 0 0 1 18 0"/><path d="M13 17l5.5-5"/><circle cx="13" cy="17" r="1.3"/></g>',
|
||||
/* 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"/>',
|
||||
@@ -4828,11 +4766,10 @@ function render() {
|
||||
else if (route.name === "trip") {
|
||||
head = ["Fahrt", "Einzelfahrt"]; v.innerHTML = vTrip(route.id);
|
||||
const t = TRIPS.find((x) => x.trip_id === route.id);
|
||||
if (t) fahrtOrteNachtragen(t);
|
||||
}
|
||||
else if (route.name === "fill") { head = ["Tankvorgang", "Einzelbeleg"]; v.innerHTML = vFill(route.id); }
|
||||
else if (route.name === "service") { head = ["Wartung", "Service"]; v.innerHTML = vService(); }
|
||||
else if (route.name === "reifen") { head = ["Fahrzeug", "Reifen"]; v.innerHTML = vReifen(); }
|
||||
else if (route.name === "reifen") { head = ["Fahrzeug", "Räder"]; v.innerHTML = vReifen(); }
|
||||
else if (route.name === "einst") { head = ["App", "Einstellungen"]; v.innerHTML = vEinst(); }
|
||||
else if (route.name === "ident") { head = ["Fahrzeug", "Identität und Technik"]; v.innerHTML = vIdent(); }
|
||||
else if (route.name === "sicherheit") { head = ["Fahrzeug", "Fahrzeugstatus"]; v.innerHTML = vSicherheit(); }
|
||||
@@ -5757,7 +5694,7 @@ function ereignisseVerdrahten() {
|
||||
const rfArchDel = e.target.closest("[data-reifenarchivloeschen]");
|
||||
if (rfArchDel) {
|
||||
const id = rfArchDel.dataset.reifenarchivloeschen;
|
||||
bestaetigen("Archivierten Reifensatz löschen?",
|
||||
bestaetigen("Archivierten Radsatz löschen?",
|
||||
"Das lässt sich nicht widerrufen.", "Löschen", () => {
|
||||
serviceRufen("reifen_archiv_loeschen", { id });
|
||||
CAR.reifen.archiv = CAR.reifen.archiv.filter((a) => a.id !== id);
|
||||
@@ -5778,10 +5715,9 @@ function ereignisseVerdrahten() {
|
||||
} else {
|
||||
const datum = $("#rfDat").value;
|
||||
CAR.reifen.wechsel.datum = datum;
|
||||
CAR.reifen.wechsel.vereinbart = $("#rfOk").checked;
|
||||
profilSpeichern();
|
||||
const ziel = CAR.reifen.aktiv === "Sommer" ? "Winter" : "Sommer";
|
||||
ics(`${CONFIG.fahrzeugtitel}: Reifenwechsel auf ${ziel}`, datum, CAR.service.autohaus.name, `Umstellung des Zählers auf den ${ziel}satz nicht vergessen`);
|
||||
ics(`${CONFIG.fahrzeugtitel}: Radwechsel auf ${ziel}`, datum, CAR.service.autohaus.name, `Umstellung des Zählers auf den ${ziel}satz nicht vergessen`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user