Einzelbeleg: CI poi pin from the receipt address, plus a centre button

The station marker was a red circleMarker. It is now the CI poi pin
(poi-l/poi-s via tankstellenMarkerSVG) - the plain pin rather than poi-car, so
the vehicle and the station stay distinguishable on a map.

While testing it turned out nothing in the backend ever writes station_lat /
station_lon, so that marker had never been reachable in practice. The receipt
address is now geocoded in the frontend via Nominatim /search
(adresseAufloesen, the forward counterpart to the existing reverse lookup),
with results cached in localStorage - negative ones too, since a station
address does not move and Nominatim's terms ask for caching. Verified live:
"Shell, SÖLDEN" resolves to 46.9756 / 11.0111.

The map card gained a centre button (CI.gps, which is byte-identical to the
delivered gps-s.svg), disabled until the coordinates arrive. .mapbox needed
position:relative so the control anchors to the card instead of an ancestor.
Only in the Einzelbeleg, as requested - the Standort fullscreen already has
its own centre controls.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 13:15:20 +02:00
parent 484f2ffe6d
commit 899b770cfa
7 changed files with 292 additions and 10 deletions
@@ -386,6 +386,8 @@ function ring(pct) {
/* ------------------------------------------------------------------ Karte
Leaflet weiterhin per CDN geladen (siehe Kopfkommentar). */
let MAP = null, TILE = null, LEAFLET_LADEND = null;
// Zielpunkt des Zentrieren-Knopfs im Einzelbeleg (siehe mapBoxMitZentrieren).
let BELEG_STATION_POS = null;
/* Bewusst themeunabhaengig hell: Kartendienste (Google Maps & Co.) zeigen die
Standard-Strassenkarte immer im hellen Stil, weil Strassen, Beschriftungen
und Wegfarben darauf lesbar bleiben. Die frueher bei Nacht eingeblendeten
@@ -441,6 +443,21 @@ function leafletLaden() {
function mapBox() { return `<div class="mapbox"><div id="map"></div></div>`; }
/* Wie mapBox(), aber mit Zentrieren-Knopf - nur im Einzelbeleg. Der Knopf
holt die Tankstelle zurück in die Mitte, nachdem man auf der Karte
herumgezogen hat. Er sitzt im .mapbox (position:relative), damit er über
der Karte schwebt, und ist bis zum Eintreffen der Koordinaten deaktiviert -
die kommen bei Belegen ohne station_lat erst aus dem Nominatim-Abruf
(siehe adresseAufloesen). */
function mapBoxMitZentrieren() {
return `<div class="mapbox">
<div id="map"></div>
<div class="kartensteuerung kartensteuerung-klein">
<button type="button" id="belegZentrieren" aria-label="Auf die Tankstelle zentrieren" disabled>${ciSVG(CI.gps, 20)}</button>
</div>
</div>`;
}
/* Kartenmittelpunkt ohne eigene Positionsdaten (z. B. Fahrt ohne GPS-Punkte):
die Werkstatt-Koordinaten aus dem Profil statt einer im Code fest
hinterlegten Region - die liegen ohnehin meist in der Nähe und kommen
@@ -487,10 +504,21 @@ async function initMap() {
MAP.setView(kartenMittelpunkt(), 11); // kein Track ohne GPS-Daten (§7a-Backend-Lücke)
}
} else if (route.name === "fill") {
BELEG_STATION_POS = null; // gehört zum vorherigen Beleg, sonst zentriert der Knopf falsch
const f = FILLS.find((x) => x.tank_id === route.id);
if (f && f.station_lat != null) {
MAP.setView([f.station_lat, f.station_lon], 14);
window.L.circleMarker([f.station_lat, f.station_lon], { radius: 7, color: rot, weight: 2, fillColor: rot, fillOpacity: 0.85 }).addTo(MAP).bindPopup((f.station_name || "") + "<br>" + (f.station_address || ""));
tankstelleAufKarteZeigen(MAP, { lat: f.station_lat, lon: f.station_lon }, f);
} else if (f && (f.station_address || f.station_name)) {
/* Ohne Koordinaten die Anschrift vom Beleg nachschlagen. Bis die Antwort
da ist, bleibt der neutrale Ausschnitt stehen - und die Karte könnte
inzwischen zu einer anderen Ansicht gehören, deshalb vor dem Zeichnen
prüfen, ob wir noch auf demselben Beleg sind. */
MAP.setView(kartenMittelpunkt(), 11);
const karte = MAP, belegId = route.id;
adresseAufloesen(f.station_address || f.station_name).then((pos) => {
if (!pos || MAP !== karte || route.name !== "fill" || route.id !== belegId) return;
tankstelleAufKarteZeigen(karte, pos, f);
});
} else {
MAP.setView(kartenMittelpunkt(), 11);
}
@@ -535,6 +563,15 @@ function fahrzeugMarkerSVG(px = 34) {
return `<svg viewBox="0 0 ${box} ${box}" width="${px}" height="${px}" fill="none" aria-hidden="true">${gross ? CI.poiCarL : CI.poiCar}</svg>`;
}
/* Tankstellenposition im Einzelbeleg - dieselbe Nadelform wie beim Fahrzeug,
nur ohne Auto-Glyph (CI.poiL/poiS), damit beide Marker sofort auseinander-
zuhalten sind. Spitze unten wie bei poi-car, gleiche Ankerrechnung. */
function tankstellenMarkerSVG(px = 32) {
const gross = px >= 34;
const box = gross ? 48 : 24;
return `<svg viewBox="0 0 ${box} ${box}" width="${px}" height="${px}" fill="none" aria-hidden="true">${gross ? CI.poiL : CI.poiS}</svg>`;
}
/* Fahrzustand fürs Menü: "fährt" solange die aktuelle Fahrt noch offen ist
(siehe fahrterkennung.py), sonst "steht". Ohne echtes Bewegungssignal vom
Fahrzeug (siehe AGENTS.md, FMM003/flespi offen) ist das die beste Näherung
@@ -641,6 +678,76 @@ function standortAdresseLaden() {
});
}
/* Gegenrichtung zu standortAdresseAufloesen: Adresse -> Koordinaten, für die
Tankstellennadel im Einzelbeleg. Nötig, weil der Beleg nur eine Anschrift
trägt und nichts im Backend station_lat/station_lon je befüllt - ohne diesen
Schritt bliebe die Karte dort dauerhaft ohne Nadel.
Ergebnisse landen in localStorage statt nur im RAM: eine Tankstellenadresse
ändert ihre Lage nicht, und der Einzelbeleg wird oft genug erneut geöffnet,
dass ein Cache nur für die laufende Sitzung Nominatim unnötig oft anfassen
würde (deren Nutzungsbedingungen bitten ausdrücklich darum, Ergebnisse zu
cachen). Auch ein Fehlschlag wird vermerkt - als null -, damit eine Adresse,
die Nominatim nicht kennt, nicht bei jedem Öffnen erneut abgefragt wird. */
const GEO_CACHE_KEY = "audi_dashboard_geocache";
let GEO_CACHE = null;
function geoCacheLesen() {
if (GEO_CACHE) return GEO_CACHE;
try { GEO_CACHE = JSON.parse(localStorage.getItem(GEO_CACHE_KEY)) || {}; }
catch (e) { GEO_CACHE = {}; }
return GEO_CACHE;
}
function geoCacheSchreiben(schluessel, wert) {
const cache = geoCacheLesen();
cache[schluessel] = wert;
try { localStorage.setItem(GEO_CACHE_KEY, JSON.stringify(cache)); }
catch (e) { /* voller/gesperrter Speicher: Cache bleibt dann RAM-only */ }
}
async function adresseAufloesen(adresse) {
const suche = (adresse || "").trim();
if (!suche) return null;
const cache = geoCacheLesen();
if (Object.prototype.hasOwnProperty.call(cache, suche)) return cache[suche];
let treffer = null;
try {
const res = await fetch(
`https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(suche)}`,
{ headers: { "Accept-Language": "de" } }
);
const daten = await res.json();
if (Array.isArray(daten) && daten.length) {
treffer = { lat: parseFloat(daten[0].lat), lon: parseFloat(daten[0].lon) };
}
} catch (e) {
// Netzfehler ist kein "Adresse gibt es nicht" - nichts merken, damit der
// nächste Aufruf es erneut versucht (vgl. standortAdresseAufloesen).
return null;
}
geoCacheSchreiben(suche, treffer);
return treffer;
}
/* Setzt Nadel und Ausschnitt, sobald die Koordinaten da sind. Getrennt von
initMap(), weil der Abruf asynchron ist und die Karte bis dahin schon
steht. */
function tankstelleAufKarteZeigen(karte, pos, f) {
if (!karte || !window.L || !pos) return;
karte.setView([pos.lat, pos.lon], 14);
const icon = window.L.divIcon({ className: "fahrzeug-pin-wrap", html: `<div class="fahrzeug-pin">${tankstellenMarkerSVG(32)}</div>`, iconSize: [32, 32], iconAnchor: [15, 31] });
window.L.marker([pos.lat, pos.lon], { icon }).addTo(karte)
.bindPopup((f.station_name || "") + "<br>" + (f.station_address || ""));
// Position merken und den Knopf scharf schalten - er kann erst jetzt etwas
// Sinnvolles tun (bei Adressauflösung kommt das hier verzögert, siehe
// Aufrufstelle in initMap).
BELEG_STATION_POS = pos;
const knopf = ROOT && ROOT.getElementById("belegZentrieren");
if (knopf) knopf.disabled = false;
}
function belegAufTankstelleZentrieren() {
if (!MAP || !BELEG_STATION_POS) return;
MAP.flyTo([BELEG_STATION_POS.lat, BELEG_STATION_POS.lon], Math.max(MAP.getZoom(), 14));
}
function standortKartenlink() {
return standortBekannt() ? `https://www.google.com/maps/dir/?api=1&destination=${CAR.standortLat},${CAR.standortLon}` : null;
}
@@ -814,6 +921,12 @@ const CI = {
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"/>',
/* poi-car-l - grosse Variante desselben CI-Symbols (48er-Raster), fuer die
Fahrzeugnadel auf der Vollbildkarte. Siehe fahrzeugMarkerSVG(). */
/* poi-l/poi-s aus dem CI-Satz: neutrale Kartennadel mit Kreis, fuer die
Tankstellenposition im Einzelbeleg (poi-car bleibt dem Fahrzeug
vorbehalten, sonst waeren beide Nadeln auf einer Karte nicht zu
unterscheiden). Siehe tankstellenMarkerSVG(). */
poiL: '<path fill="currentColor" d="M9 17.5C9 9.52 15.52 3 23.5 3S38 9.52 38 17.5c0 2.04-.92 4.78-2.23 7.67a90 90 0 0 1-4.84 8.98 170 170 0 0 1-6.98 10.59l-.03.04-.01.01-.41-.29-.4.3-.02-.02-.03-.04a66 66 0 0 1-.56-.8 166 166 0 0 1-6.42-9.8 90 90 0 0 1-4.84-8.97C9.92 22.27 9 19.54 9 17.5m14.5 27-.4.3.4.55.4-.56zm0-.86a132 132 0 0 0 1.77-2.56 170 170 0 0 0 4.8-7.45 89 89 0 0 0 4.79-8.87c1.3-2.89 2.14-5.45 2.14-7.26C37 10.08 30.92 4 23.5 4S10 10.08 10 17.5c0 1.8.83 4.37 2.14 7.26a89 89 0 0 0 4.79 8.87 170 170 0 0 0 6.57 10.01M16 17.5a7.5 7.5 0 1 1 15 0 7.5 7.5 0 0 1-15 0m7.5-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13"/>',
poiS: '<path fill="currentColor" d="M4 8.5C4 4.32 7.32 1 11.5 1S19 4.32 19 8.5c0 1.09-.48 2.53-1.14 4.03a49 49 0 0 1-2.43 4.68A92 92 0 0 1 12 22.67l-.06.08-.01.03-.42-.28-.41.28-.02-.03-.06-.08A66 66 0 0 1 10 21.13a92 92 0 0 1-2.42-3.92 49 49 0 0 1-2.43-4.68C4.48 11.03 4 9.6 4 8.5m7.5 14-.41.28.41.6.41-.6zm0-.89a92 92 0 0 0 3.07-4.9c.87-1.5 1.73-3.1 2.38-4.58A10.4 10.4 0 0 0 18 8.5C18 4.88 15.12 2 11.5 2A6.46 6.46 0 0 0 5 8.5c0 .86.4 2.14 1.05 3.63a48 48 0 0 0 2.38 4.58 91 91 0 0 0 3.07 4.9M8 8.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0M11.5 6a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5"/>',
poiCarL: '<path fill="currentColor" d="M9 17.5C9 9.52 15.52 3 23.5 3S38 9.52 38 17.5c0 2.04-.92 4.78-2.23 7.67a90 90 0 0 1-4.84 8.98 170 170 0 0 1-6.98 10.59l-.03.04-.01.01-.41-.29-.4.3-.02-.02-.03-.04a66 66 0 0 1-.56-.8 166 166 0 0 1-6.42-9.8 90 90 0 0 1-4.84-8.97C9.92 22.27 9 19.54 9 17.5m14.5 27-.4.3.4.55.4-.56zm0-.86a132 132 0 0 0 1.77-2.56 170 170 0 0 0 4.8-7.45 89 89 0 0 0 4.79-8.87c1.3-2.89 2.14-5.45 2.14-7.26C37 10.08 30.92 4 23.5 4S10 10.08 10 17.5c0 1.8.83 4.37 2.14 7.26a89 89 0 0 0 4.79 8.87 170 170 0 0 0 6.57 10.01M17.96 7.98A63.3 63.3 0 0 1 29.05 8l.2.02.13.16c.6.7 1.1 1.36 1.46 1.94l1.31 2.08L34 14.31v7.19c0 .83-.67 1.5-1.5 1.5h-2a1.5 1.5 0 0 1-1.5-1.5V21H18v.5c0 .83-.67 1.5-1.5 1.5h-2a1.5 1.5 0 0 1-1.5-1.5v-7.19l1.85-2.11 1.3-2.08c.37-.58.86-1.24 1.47-1.96l.13-.16.2-.02ZM15.48 13 14 14.69V15h3v1h-3v5.5c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5V20h13v1.5c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5V16h-3v-1h3v-.31L31.52 13zm15.36-1L30 10.65a16 16 0 0 0-1.25-1.67 62.4 62.4 0 0 0-10.5-.02 17 17 0 0 0-1.25 1.7L16.16 12zM19.3 15h8.42l1.4 1.4-.16.31c-.34.74-.65 1.32-.92 1.72-.14.2-.28.38-.42.5a.92.92 0 0 1-.61.27h-7a.92.92 0 0 1-.6-.27 2.4 2.4 0 0 1-.43-.5c-.27-.4-.58-.98-.92-1.72l-.15-.32zm.42 1-.6.6c.26.57.5.99.69 1.27.11.17.2.27.26.32l.01.01h6.86v-.01c.07-.05.16-.15.27-.32.2-.28.43-.7.7-1.27l-.6-.6h-7.6Z"/>',
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"/>',
@@ -2735,7 +2848,7 @@ function vFill(id) {
}
return `
${mapBox()}
${mapBoxMitZentrieren()}
<div class="tile flat" style="margin:18px 0 14px">
<span class="label">Getankt</span>
<div class="fig" style="font-size:52px;margin-top:10px">${f.liters != null ? de(f.liters, 2) : ""}<span class="unit">l</span></div>
@@ -3669,6 +3782,9 @@ function ereignisseVerdrahten() {
if (e.target.closest("[data-standort-beide]")) { standortBeideZeigen(); return; }
if (e.target.closest("[data-standort-schliessen]")) { standortMenuSchliessen(); return; }
if (e.target.closest("[data-standort-teilen]")) { standortTeilen(); return; }
// Einzelbeleg: zurück auf die Tankstelle (manipuliert MAP direkt, kein
// render() - das würde die Karte neu aufbauen und den Ausschnitt verlieren).
if (e.target.closest("#belegZentrieren")) { belegAufTankstelleZentrieren(); return; }
const g = e.target.closest("[data-go]");
if (g) { const [n, i] = g.dataset.go.split(":"); go(n, i); }
});
@@ -1 +1 @@
{"version": 1786766000}
{"version": 1786769000}
@@ -688,7 +688,10 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
.swipe-wrap.swiped .swipe-content { transform: translateX(-84px); }
/* -------------------------------------------------------------- Karte */
.mapbox { border-radius: var(--r-tile); overflow: hidden; height: 210px; background: var(--tile); }
/* position:relative, damit der Zentrieren-Knopf im Einzelbeleg (absolut
positioniert, siehe .kartensteuerung-klein) sich an dieser Kachel ausrichtet
und nicht am naechsten Vorfahren weiter oben. */
.mapbox { position: relative; border-radius: var(--r-tile); overflow: hidden; height: 210px; background: var(--tile); }
.mapbox #map { width: 100%; height: 100%; }
/* --------------------------------------------------------- Standort
@@ -757,6 +760,12 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
}
.kartensteuerung button:active { transform: scale(.94); }
:host([data-theme="tag"]) .kartensteuerung button:active { background: #fff; }
/* Variante in der Belegkarte: die Karte ist dort nur 210px hoch, ein 48er-
Knopf mit 16px Abstand wuerde ein Viertel davon verdecken. */
.kartensteuerung-klein { top: 10px; right: 10px; }
.kartensteuerung-klein button { width: 36px; height: 36px; }
.kartensteuerung-klein button:disabled { opacity: .45; cursor: default; }
.kartensteuerung-klein button:disabled:active { transform: none; }
/* Sichtbarer Anteil im geschlossenen Zustand (STANDORT_PEEK_PX im Frontend) -
beide Werte müssen zusammenpassen, siehe Kommentar dort. Der Rest des
+119 -3
View File
@@ -386,6 +386,8 @@ function ring(pct) {
/* ------------------------------------------------------------------ Karte
Leaflet weiterhin per CDN geladen (siehe Kopfkommentar). */
let MAP = null, TILE = null, LEAFLET_LADEND = null;
// Zielpunkt des Zentrieren-Knopfs im Einzelbeleg (siehe mapBoxMitZentrieren).
let BELEG_STATION_POS = null;
/* Bewusst themeunabhaengig hell: Kartendienste (Google Maps & Co.) zeigen die
Standard-Strassenkarte immer im hellen Stil, weil Strassen, Beschriftungen
und Wegfarben darauf lesbar bleiben. Die frueher bei Nacht eingeblendeten
@@ -441,6 +443,21 @@ function leafletLaden() {
function mapBox() { return `<div class="mapbox"><div id="map"></div></div>`; }
/* Wie mapBox(), aber mit Zentrieren-Knopf - nur im Einzelbeleg. Der Knopf
holt die Tankstelle zurück in die Mitte, nachdem man auf der Karte
herumgezogen hat. Er sitzt im .mapbox (position:relative), damit er über
der Karte schwebt, und ist bis zum Eintreffen der Koordinaten deaktiviert -
die kommen bei Belegen ohne station_lat erst aus dem Nominatim-Abruf
(siehe adresseAufloesen). */
function mapBoxMitZentrieren() {
return `<div class="mapbox">
<div id="map"></div>
<div class="kartensteuerung kartensteuerung-klein">
<button type="button" id="belegZentrieren" aria-label="Auf die Tankstelle zentrieren" disabled>${ciSVG(CI.gps, 20)}</button>
</div>
</div>`;
}
/* Kartenmittelpunkt ohne eigene Positionsdaten (z. B. Fahrt ohne GPS-Punkte):
die Werkstatt-Koordinaten aus dem Profil statt einer im Code fest
hinterlegten Region - die liegen ohnehin meist in der Nähe und kommen
@@ -487,10 +504,21 @@ async function initMap() {
MAP.setView(kartenMittelpunkt(), 11); // kein Track ohne GPS-Daten (§7a-Backend-Lücke)
}
} else if (route.name === "fill") {
BELEG_STATION_POS = null; // gehört zum vorherigen Beleg, sonst zentriert der Knopf falsch
const f = FILLS.find((x) => x.tank_id === route.id);
if (f && f.station_lat != null) {
MAP.setView([f.station_lat, f.station_lon], 14);
window.L.circleMarker([f.station_lat, f.station_lon], { radius: 7, color: rot, weight: 2, fillColor: rot, fillOpacity: 0.85 }).addTo(MAP).bindPopup((f.station_name || "") + "<br>" + (f.station_address || ""));
tankstelleAufKarteZeigen(MAP, { lat: f.station_lat, lon: f.station_lon }, f);
} else if (f && (f.station_address || f.station_name)) {
/* Ohne Koordinaten die Anschrift vom Beleg nachschlagen. Bis die Antwort
da ist, bleibt der neutrale Ausschnitt stehen - und die Karte könnte
inzwischen zu einer anderen Ansicht gehören, deshalb vor dem Zeichnen
prüfen, ob wir noch auf demselben Beleg sind. */
MAP.setView(kartenMittelpunkt(), 11);
const karte = MAP, belegId = route.id;
adresseAufloesen(f.station_address || f.station_name).then((pos) => {
if (!pos || MAP !== karte || route.name !== "fill" || route.id !== belegId) return;
tankstelleAufKarteZeigen(karte, pos, f);
});
} else {
MAP.setView(kartenMittelpunkt(), 11);
}
@@ -535,6 +563,15 @@ function fahrzeugMarkerSVG(px = 34) {
return `<svg viewBox="0 0 ${box} ${box}" width="${px}" height="${px}" fill="none" aria-hidden="true">${gross ? CI.poiCarL : CI.poiCar}</svg>`;
}
/* Tankstellenposition im Einzelbeleg - dieselbe Nadelform wie beim Fahrzeug,
nur ohne Auto-Glyph (CI.poiL/poiS), damit beide Marker sofort auseinander-
zuhalten sind. Spitze unten wie bei poi-car, gleiche Ankerrechnung. */
function tankstellenMarkerSVG(px = 32) {
const gross = px >= 34;
const box = gross ? 48 : 24;
return `<svg viewBox="0 0 ${box} ${box}" width="${px}" height="${px}" fill="none" aria-hidden="true">${gross ? CI.poiL : CI.poiS}</svg>`;
}
/* Fahrzustand fürs Menü: "fährt" solange die aktuelle Fahrt noch offen ist
(siehe fahrterkennung.py), sonst "steht". Ohne echtes Bewegungssignal vom
Fahrzeug (siehe AGENTS.md, FMM003/flespi offen) ist das die beste Näherung
@@ -641,6 +678,76 @@ function standortAdresseLaden() {
});
}
/* Gegenrichtung zu standortAdresseAufloesen: Adresse -> Koordinaten, für die
Tankstellennadel im Einzelbeleg. Nötig, weil der Beleg nur eine Anschrift
trägt und nichts im Backend station_lat/station_lon je befüllt - ohne diesen
Schritt bliebe die Karte dort dauerhaft ohne Nadel.
Ergebnisse landen in localStorage statt nur im RAM: eine Tankstellenadresse
ändert ihre Lage nicht, und der Einzelbeleg wird oft genug erneut geöffnet,
dass ein Cache nur für die laufende Sitzung Nominatim unnötig oft anfassen
würde (deren Nutzungsbedingungen bitten ausdrücklich darum, Ergebnisse zu
cachen). Auch ein Fehlschlag wird vermerkt - als null -, damit eine Adresse,
die Nominatim nicht kennt, nicht bei jedem Öffnen erneut abgefragt wird. */
const GEO_CACHE_KEY = "audi_dashboard_geocache";
let GEO_CACHE = null;
function geoCacheLesen() {
if (GEO_CACHE) return GEO_CACHE;
try { GEO_CACHE = JSON.parse(localStorage.getItem(GEO_CACHE_KEY)) || {}; }
catch (e) { GEO_CACHE = {}; }
return GEO_CACHE;
}
function geoCacheSchreiben(schluessel, wert) {
const cache = geoCacheLesen();
cache[schluessel] = wert;
try { localStorage.setItem(GEO_CACHE_KEY, JSON.stringify(cache)); }
catch (e) { /* voller/gesperrter Speicher: Cache bleibt dann RAM-only */ }
}
async function adresseAufloesen(adresse) {
const suche = (adresse || "").trim();
if (!suche) return null;
const cache = geoCacheLesen();
if (Object.prototype.hasOwnProperty.call(cache, suche)) return cache[suche];
let treffer = null;
try {
const res = await fetch(
`https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=${encodeURIComponent(suche)}`,
{ headers: { "Accept-Language": "de" } }
);
const daten = await res.json();
if (Array.isArray(daten) && daten.length) {
treffer = { lat: parseFloat(daten[0].lat), lon: parseFloat(daten[0].lon) };
}
} catch (e) {
// Netzfehler ist kein "Adresse gibt es nicht" - nichts merken, damit der
// nächste Aufruf es erneut versucht (vgl. standortAdresseAufloesen).
return null;
}
geoCacheSchreiben(suche, treffer);
return treffer;
}
/* Setzt Nadel und Ausschnitt, sobald die Koordinaten da sind. Getrennt von
initMap(), weil der Abruf asynchron ist und die Karte bis dahin schon
steht. */
function tankstelleAufKarteZeigen(karte, pos, f) {
if (!karte || !window.L || !pos) return;
karte.setView([pos.lat, pos.lon], 14);
const icon = window.L.divIcon({ className: "fahrzeug-pin-wrap", html: `<div class="fahrzeug-pin">${tankstellenMarkerSVG(32)}</div>`, iconSize: [32, 32], iconAnchor: [15, 31] });
window.L.marker([pos.lat, pos.lon], { icon }).addTo(karte)
.bindPopup((f.station_name || "") + "<br>" + (f.station_address || ""));
// Position merken und den Knopf scharf schalten - er kann erst jetzt etwas
// Sinnvolles tun (bei Adressauflösung kommt das hier verzögert, siehe
// Aufrufstelle in initMap).
BELEG_STATION_POS = pos;
const knopf = ROOT && ROOT.getElementById("belegZentrieren");
if (knopf) knopf.disabled = false;
}
function belegAufTankstelleZentrieren() {
if (!MAP || !BELEG_STATION_POS) return;
MAP.flyTo([BELEG_STATION_POS.lat, BELEG_STATION_POS.lon], Math.max(MAP.getZoom(), 14));
}
function standortKartenlink() {
return standortBekannt() ? `https://www.google.com/maps/dir/?api=1&destination=${CAR.standortLat},${CAR.standortLon}` : null;
}
@@ -814,6 +921,12 @@ const CI = {
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"/>',
/* poi-car-l - grosse Variante desselben CI-Symbols (48er-Raster), fuer die
Fahrzeugnadel auf der Vollbildkarte. Siehe fahrzeugMarkerSVG(). */
/* poi-l/poi-s aus dem CI-Satz: neutrale Kartennadel mit Kreis, fuer die
Tankstellenposition im Einzelbeleg (poi-car bleibt dem Fahrzeug
vorbehalten, sonst waeren beide Nadeln auf einer Karte nicht zu
unterscheiden). Siehe tankstellenMarkerSVG(). */
poiL: '<path fill="currentColor" d="M9 17.5C9 9.52 15.52 3 23.5 3S38 9.52 38 17.5c0 2.04-.92 4.78-2.23 7.67a90 90 0 0 1-4.84 8.98 170 170 0 0 1-6.98 10.59l-.03.04-.01.01-.41-.29-.4.3-.02-.02-.03-.04a66 66 0 0 1-.56-.8 166 166 0 0 1-6.42-9.8 90 90 0 0 1-4.84-8.97C9.92 22.27 9 19.54 9 17.5m14.5 27-.4.3.4.55.4-.56zm0-.86a132 132 0 0 0 1.77-2.56 170 170 0 0 0 4.8-7.45 89 89 0 0 0 4.79-8.87c1.3-2.89 2.14-5.45 2.14-7.26C37 10.08 30.92 4 23.5 4S10 10.08 10 17.5c0 1.8.83 4.37 2.14 7.26a89 89 0 0 0 4.79 8.87 170 170 0 0 0 6.57 10.01M16 17.5a7.5 7.5 0 1 1 15 0 7.5 7.5 0 0 1-15 0m7.5-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13"/>',
poiS: '<path fill="currentColor" d="M4 8.5C4 4.32 7.32 1 11.5 1S19 4.32 19 8.5c0 1.09-.48 2.53-1.14 4.03a49 49 0 0 1-2.43 4.68A92 92 0 0 1 12 22.67l-.06.08-.01.03-.42-.28-.41.28-.02-.03-.06-.08A66 66 0 0 1 10 21.13a92 92 0 0 1-2.42-3.92 49 49 0 0 1-2.43-4.68C4.48 11.03 4 9.6 4 8.5m7.5 14-.41.28.41.6.41-.6zm0-.89a92 92 0 0 0 3.07-4.9c.87-1.5 1.73-3.1 2.38-4.58A10.4 10.4 0 0 0 18 8.5C18 4.88 15.12 2 11.5 2A6.46 6.46 0 0 0 5 8.5c0 .86.4 2.14 1.05 3.63a48 48 0 0 0 2.38 4.58 91 91 0 0 0 3.07 4.9M8 8.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0M11.5 6a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5"/>',
poiCarL: '<path fill="currentColor" d="M9 17.5C9 9.52 15.52 3 23.5 3S38 9.52 38 17.5c0 2.04-.92 4.78-2.23 7.67a90 90 0 0 1-4.84 8.98 170 170 0 0 1-6.98 10.59l-.03.04-.01.01-.41-.29-.4.3-.02-.02-.03-.04a66 66 0 0 1-.56-.8 166 166 0 0 1-6.42-9.8 90 90 0 0 1-4.84-8.97C9.92 22.27 9 19.54 9 17.5m14.5 27-.4.3.4.55.4-.56zm0-.86a132 132 0 0 0 1.77-2.56 170 170 0 0 0 4.8-7.45 89 89 0 0 0 4.79-8.87c1.3-2.89 2.14-5.45 2.14-7.26C37 10.08 30.92 4 23.5 4S10 10.08 10 17.5c0 1.8.83 4.37 2.14 7.26a89 89 0 0 0 4.79 8.87 170 170 0 0 0 6.57 10.01M17.96 7.98A63.3 63.3 0 0 1 29.05 8l.2.02.13.16c.6.7 1.1 1.36 1.46 1.94l1.31 2.08L34 14.31v7.19c0 .83-.67 1.5-1.5 1.5h-2a1.5 1.5 0 0 1-1.5-1.5V21H18v.5c0 .83-.67 1.5-1.5 1.5h-2a1.5 1.5 0 0 1-1.5-1.5v-7.19l1.85-2.11 1.3-2.08c.37-.58.86-1.24 1.47-1.96l.13-.16.2-.02ZM15.48 13 14 14.69V15h3v1h-3v5.5c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5V20h13v1.5c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5V16h-3v-1h3v-.31L31.52 13zm15.36-1L30 10.65a16 16 0 0 0-1.25-1.67 62.4 62.4 0 0 0-10.5-.02 17 17 0 0 0-1.25 1.7L16.16 12zM19.3 15h8.42l1.4 1.4-.16.31c-.34.74-.65 1.32-.92 1.72-.14.2-.28.38-.42.5a.92.92 0 0 1-.61.27h-7a.92.92 0 0 1-.6-.27 2.4 2.4 0 0 1-.43-.5c-.27-.4-.58-.98-.92-1.72l-.15-.32zm.42 1-.6.6c.26.57.5.99.69 1.27.11.17.2.27.26.32l.01.01h6.86v-.01c.07-.05.16-.15.27-.32.2-.28.43-.7.7-1.27l-.6-.6h-7.6Z"/>',
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"/>',
@@ -2735,7 +2848,7 @@ function vFill(id) {
}
return `
${mapBox()}
${mapBoxMitZentrieren()}
<div class="tile flat" style="margin:18px 0 14px">
<span class="label">Getankt</span>
<div class="fig" style="font-size:52px;margin-top:10px">${f.liters != null ? de(f.liters, 2) : ""}<span class="unit">l</span></div>
@@ -3669,6 +3782,9 @@ function ereignisseVerdrahten() {
if (e.target.closest("[data-standort-beide]")) { standortBeideZeigen(); return; }
if (e.target.closest("[data-standort-schliessen]")) { standortMenuSchliessen(); return; }
if (e.target.closest("[data-standort-teilen]")) { standortTeilen(); return; }
// Einzelbeleg: zurück auf die Tankstelle (manipuliert MAP direkt, kein
// render() - das würde die Karte neu aufbauen und den Ausschnitt verlieren).
if (e.target.closest("#belegZentrieren")) { belegAufTankstelleZentrieren(); return; }
const g = e.target.closest("[data-go]");
if (g) { const [n, i] = g.dataset.go.split(":"); go(n, i); }
});
@@ -1 +1 @@
{"version": 1786766000}
{"version": 1786769000}
+10 -1
View File
@@ -688,7 +688,10 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
.swipe-wrap.swiped .swipe-content { transform: translateX(-84px); }
/* -------------------------------------------------------------- Karte */
.mapbox { border-radius: var(--r-tile); overflow: hidden; height: 210px; background: var(--tile); }
/* position:relative, damit der Zentrieren-Knopf im Einzelbeleg (absolut
positioniert, siehe .kartensteuerung-klein) sich an dieser Kachel ausrichtet
und nicht am naechsten Vorfahren weiter oben. */
.mapbox { position: relative; border-radius: var(--r-tile); overflow: hidden; height: 210px; background: var(--tile); }
.mapbox #map { width: 100%; height: 100%; }
/* --------------------------------------------------------- Standort
@@ -757,6 +760,12 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
}
.kartensteuerung button:active { transform: scale(.94); }
:host([data-theme="tag"]) .kartensteuerung button:active { background: #fff; }
/* Variante in der Belegkarte: die Karte ist dort nur 210px hoch, ein 48er-
Knopf mit 16px Abstand wuerde ein Viertel davon verdecken. */
.kartensteuerung-klein { top: 10px; right: 10px; }
.kartensteuerung-klein button { width: 36px; height: 36px; }
.kartensteuerung-klein button:disabled { opacity: .45; cursor: default; }
.kartensteuerung-klein button:disabled:active { transform: none; }
/* Sichtbarer Anteil im geschlossenen Zustand (STANDORT_PEEK_PX im Frontend) -
beide Werte müssen zusammenpassen, siehe Kommentar dort. Der Rest des