a99ae432fc
Fuenf Meldungen des Eigentuemers, vier davon behoben: 1. Tankstellen-Nadel war bei 32 px ein dunkler Fleck (poi zeichnet nur einen Ring). Gefuellter Punkt in currentColor zwischen Silhouette und Symbol - die Geometrie stand im Pfad. Rot wurde ausdruecklich abgelehnt, die Umsetzung aendert keine Farbe. 2. Tanken-Symbol im Standort-Blatt wurde gefuellt UND umrandet, weil das CI-Symbol zusaetzlich stroke-Attribute mitbekam. Jetzt ueber ciSVG wie das Parkplatz-Symbol daneben; in der App dafuer imRaster an SymbolTanken (der 0,84-Faktor gilt nur in der Reiterleiste). 3. Tankstellenname: der Bestand hat zwei Bauformen - aeltere Belege tragen den Betreiber in station_name und die Anschrift daneben. Neu tankstelleTeile() liest beide Felder, entfernt Firmierungen, kuerzt Strassen und trennt die Hausnummer auch ohne Leerzeichen. Uebersicht: Marke, Strasse, Ort. Einzelbeleg: zwei Zeilen. adresseTeilen() hat keinen Aufrufer mehr und ist entfernt. 4. Zwischenablage und Kalendereintrag hatten denselben Grund: Capacitor 8 registriert nur, was in packageClassList steht, und cap sync traegt dort ausschliesslich npm-Pakete ein. Beide app-eigenen Plugins lagen im Programm und wurden nie registriert. ios-teilen-einrichten.mjs traegt sie jetzt nach, ios-signieren.sh bricht ab, wenn sie im fertigen .ipa fehlen. 5. Geteilter Beleg nur mit Datum und Uhrzeit bleibt offen - der Weg nutzt denselben belegLesen()-Pfad wie eine gewaehlte Datei, es fehlt der konkrete Beleg oder die Protokollzeile. tsc sauber, 246 Tests, vite build sauber, Panel als Modul geparst, audi_ha_test auf 2026.9.4.20 ohne Traceback. Panel und App liefern fuer die Tankstelle byteweise dasselbe.
343 lines
14 KiB
TypeScript
343 lines
14 KiB
TypeScript
/**
|
|
* Tankstelle eines Belegs: Anzeige in zwei Zeilen und der Weg in den
|
|
* Kartendienst des Geräts.
|
|
*
|
|
* WARUM ZWEI ZEILEN
|
|
* -----------------
|
|
* Bis zum 04.09.2026 stand die Tankstelle als gewöhnliche Wertezeile rechts in
|
|
* der schmalen Wertespalte: Name oben, die komplette Anschrift als Kleintext
|
|
* darunter. Gemessen an der echten Kachel blieben dafür 120 px - "Hermann
|
|
* Mogler Mineralölg. GmbH, Heidenheimerstr.6, 89551 Königsbronn" brach dort in
|
|
* vier bis fünf Zeilen um. Die Aufteilung hier ist die vom Eigentümer
|
|
* verlangte: erste Zeile Tankstelle und Straße, zweite Zeile der Ort.
|
|
*
|
|
* WIE DIE ANSCHRIFT GETEILT WIRD
|
|
* ------------------------------
|
|
* Der Belegparser setzt sie als `"<Straße>, <PLZ Ort>"` zusammen
|
|
* (shell_beleg_parser.py). Geteilt wird deshalb am LETZTEN Komma: ein Name wie
|
|
* "Str. 5, Gewerbepark, 85057 Ingolstadt" behält so seinen Zusatz bei der
|
|
* Straße statt beim Ort. Ohne Komma bleibt alles in der ersten Zeile - lieber
|
|
* ein zu langer erster Eintrag als ein erfundener Ort.
|
|
*
|
|
* KARTENDIENST STATT EINES FESTEN ANBIETERS
|
|
* -----------------------------------------
|
|
* "Klick auf die Tankstelle soll den Standard-Kartendienst öffnen" (Eigentümer,
|
|
* 04.09.2026). Auf Apple-Geräten ist das Karten, sonst Google Maps - beides
|
|
* über die jeweilige Web-Adresse, die das System an die installierte App
|
|
* weiterreicht. Ein `geo:`-Verweis wäre der sauberere Weg, aber Safari kennt
|
|
* ihn nicht, und ein Verweis, der auf dem Hauptgerät ins Leere läuft, ist
|
|
* keiner.
|
|
*/
|
|
|
|
export interface Koordinate {
|
|
lat: number
|
|
lon: number
|
|
}
|
|
|
|
/* Kopfmarken des deutschsprachigen Marktes.
|
|
|
|
WORTGLEICH mit `_MARKEN` in custom_components/audi_dashboard/shell_beleg_parser.py
|
|
(und mit MARKEN im Panel) - der Parser erkennt die Marke beim Einlesen eines
|
|
Belegs, diese Liste erkennt sie nachträglich in dem, was gespeichert ist.
|
|
Laufen die beiden auseinander, zeigt die App eine andere Marke als der
|
|
Beleg. Reihenfolge zählt: "TotalEnergies" muss vor "Total" stehen. */
|
|
const MARKEN = [
|
|
"TotalEnergies", "Total", "Aral", "Shell", "Eni", "Agip", "Esso", "OMV",
|
|
"Avia", "Turmöl", "Turmoel", "Orlen", "Tamoil", "Westfalen", "Allguth",
|
|
"Classic", "Sprint", "Raiffeisen", "BayWa", "Elan", "HEM", "Star", "JET",
|
|
"bft", "BP", "Q1",
|
|
]
|
|
|
|
/**
|
|
* Die Kopfmarke aus dem gespeicherten Stationsnamen - `null`, wenn keine
|
|
* bekannte darin steht.
|
|
*
|
|
* WARUM ÜBERHAUPT NACHTRÄGLICH: `station_name` trägt je nach Alter des Belegs
|
|
* Verschiedenes. Seit dem 16.08.2026 setzt der Parser "Marke, Straße, Ort"
|
|
* zusammen ("Shell, SÖLDEN"); ältere Belege - und jeder ohne erkennbare
|
|
* Marke - tragen dort die Firmierung des Betreibers ("A. Zrenner GmbH",
|
|
* "Hermann Mogler Mineralölg. GmbH"). Die ist nach außen nicht sichtbar und
|
|
* gehört deshalb nicht in die Anzeige (Vorgabe des Eigentümers, 04.09.2026);
|
|
* sichtbar ist die Kopfmarke.
|
|
*
|
|
* Wortgrenzen sind Absicht: "Star" darf nicht in "Starnberg" treffen.
|
|
*/
|
|
export function markeErkennen(name: string | null | undefined): string | null {
|
|
const roh = (name ?? "").trim()
|
|
if (!roh) return null
|
|
for (const marke of MARKEN) {
|
|
const muster = new RegExp(`\\b${marke.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i")
|
|
if (muster.test(roh)) return marke
|
|
}
|
|
return null
|
|
}
|
|
|
|
/* Kennzeichen einer Firmierung. Ein Teil, der so etwas enthaelt, ist der
|
|
BETREIBER und gehoert nach Vorgabe des Eigentuemers nicht in die Anzeige -
|
|
"TC Sengül GmbH" oder "Hermann Mogler Mineralölg. GmbH" stehen auf keinem
|
|
Preisschild und in keiner Karte. */
|
|
const FIRMA = /\b(gmbh|mbh|ohg|kg|ag|e\.\s?k\.|inh\.|& co)\b/i
|
|
|
|
/* "91522 Ansbach" - vier Stellen wegen Oesterreich (der Belegparser hat
|
|
denselben Fall, siehe shell_beleg_parser.py). */
|
|
const PLZ_ORT = /^(\d{4,5})\s+(.+)$/
|
|
|
|
/* Strasse mit Hausnummer am Ende. Greedy bis zum letzten Nicht-Ziffernzeichen,
|
|
damit auch die zusammengeschriebene Form der Belege trifft:
|
|
"Nürnberger Str.74" -> "Nürnberger Str." + "74". */
|
|
const STRASSE_NR = /^(.*\D)\s*(\d+\s*[a-zA-Z]?)$/
|
|
|
|
/**
|
|
* "Nürnberger Straße" → "Nürnberger Str.", "Heidenheimerstraße" →
|
|
* "Heidenheimerstr.". Vorgabe des Eigentuemers fuer die Uebersicht; die Belege
|
|
* liefern beide Schreibweisen, je nach Kasse.
|
|
*
|
|
* Die Grossschreibung des Treffers entscheidet, ob "Str." oder "str." daraus
|
|
* wird - sonst stuende mitten im Wort ein grosses S.
|
|
*/
|
|
export function strasseKuerzen(strasse: string): string {
|
|
return strasse.replace(/stra(?:ß|ss)e\b\.?/gi, (treffer) =>
|
|
treffer[0] === treffer[0]?.toUpperCase() ? "Str." : "str.",
|
|
)
|
|
}
|
|
|
|
export interface Tankstellenteile {
|
|
/** Kopfmarke, sofern erkennbar. Nie die Firmierung des Betreibers. */
|
|
marke: string
|
|
/** Ohne Hausnummer, in der kurzen Schreibweise. */
|
|
strasse: string
|
|
hausnummer: string
|
|
plz: string
|
|
ort: string
|
|
/** Der gespeicherte Name, unveraendert - der letzte Rueckfall. */
|
|
roh: string
|
|
}
|
|
|
|
/**
|
|
* Zerlegt, was ein Beleg ueber die Tankstelle hergibt.
|
|
*
|
|
* WARUM BEIDE FELDER GELESEN WERDEN
|
|
* ---------------------------------
|
|
* Der Bestand hat zwei Bauformen, am 04.09.2026 an den echten Datensaetzen
|
|
* nachgesehen:
|
|
*
|
|
* bis 2025: station_name = "TC Sengül GmbH" (der Betreiber!)
|
|
* station_address = "Nürnberger Str.74, 91522 Ansbach"
|
|
* ab 08.2026: station_name = "Shell, Pascalstr. 8, Ingolstadt"
|
|
* station_address = null
|
|
*
|
|
* Die Markenerkennung lief bis hierher nur ueber `station_name`. Bei den
|
|
* aelteren Belegen steht die Marke dort gar nicht - deshalb blieb in der
|
|
* Uebersicht die Firmierung stehen ("...GmbH", Nutzerbefund 04.09.2026),
|
|
* waehrend die Anschrift daneben ungenutzt blieb. Gelesen werden deshalb
|
|
* beide Felder: die Anschrift zuerst (sie ist die strukturierte Quelle), der
|
|
* Name danach fuer Marke und alles, was die Anschrift nicht hergibt.
|
|
*/
|
|
export function tankstelleTeile(
|
|
name: string | null | undefined,
|
|
adresse: string | null | undefined,
|
|
): Tankstellenteile {
|
|
const teile: Tankstellenteile = {
|
|
marke: markeErkennen(name) ?? "",
|
|
strasse: "",
|
|
hausnummer: "",
|
|
plz: "",
|
|
ort: "",
|
|
roh: (name ?? "").trim(),
|
|
}
|
|
|
|
const quellen: string[] = []
|
|
for (const stueck of (adresse ?? "").split(",")) quellen.push(stueck)
|
|
for (const stueck of (name ?? "").split(",")) {
|
|
const t = stueck.trim()
|
|
// Die Marke steht schon fest; als Ort taugt sie nicht.
|
|
if (teile.marke && t.toLowerCase() === teile.marke.toLowerCase()) continue
|
|
quellen.push(t)
|
|
}
|
|
|
|
for (const stueck of quellen) {
|
|
const t = stueck.trim()
|
|
if (!t || FIRMA.test(t)) continue
|
|
|
|
const plzOrt = PLZ_ORT.exec(t)
|
|
if (plzOrt) {
|
|
if (!teile.plz) teile.plz = plzOrt[1] ?? ""
|
|
if (!teile.ort) teile.ort = (plzOrt[2] ?? "").trim()
|
|
continue
|
|
}
|
|
|
|
const mitNummer = STRASSE_NR.exec(t)
|
|
if (mitNummer) {
|
|
if (!teile.strasse) {
|
|
teile.strasse = strasseKuerzen((mitNummer[1] ?? "").trim())
|
|
teile.hausnummer = (mitNummer[2] ?? "").replace(/\s+/g, "")
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Weder Postleitzahl noch Hausnummer: der Ort. Eine Strasse ohne Nummer
|
|
// laesst sich davon nicht sicher unterscheiden - im Zweifel der Ort, weil
|
|
// er in der Uebersicht die wichtigere Angabe ist.
|
|
if (!teile.ort) teile.ort = t
|
|
}
|
|
|
|
return teile
|
|
}
|
|
|
|
/**
|
|
* Die Zeile der Uebersicht: **Marke, Strasse ohne Hausnummer, Ort ohne
|
|
* Postleitzahl** (Vorgabe des Eigentuemers, 04.09.2026).
|
|
*
|
|
* Ergibt die Zerlegung gar nichts - etwa wenn nur eine Firmierung ohne
|
|
* Anschrift gespeichert ist - bleibt der rohe Name stehen. Ihn dann
|
|
* wegzulassen hiesse, eine bekannte Tankstelle "unbekannt" zu nennen.
|
|
*/
|
|
export function tankstelleKurz(
|
|
name: string | null | undefined,
|
|
adresse: string | null | undefined,
|
|
): string {
|
|
const t = tankstelleTeile(name, adresse)
|
|
const zeile = [t.marke, t.strasse, t.ort].filter(Boolean).join(", ")
|
|
return zeile || t.roh || "unbekannt"
|
|
}
|
|
|
|
/**
|
|
* Die beiden Zeilen im Einzelbeleg (Vorgabe des Eigentuemers, 04.09.2026):
|
|
* oben **Marke, Strasse mit Hausnummer**, unten **Postleitzahl und Ort**.
|
|
*
|
|
* Geschrieben wie eine Anschrift, also "Shell, Pascalstr. 8" und "85057
|
|
* Ingolstadt" - das Komma trennt die Marke ab, zwischen Strasse und Nummer
|
|
* und zwischen Postleitzahl und Ort steht keins.
|
|
*
|
|
* `oben` ist nie leer: gibt der Beleg nur einen Ort her, rueckt der nach oben,
|
|
* und kennt er gar nichts, heisst es dort "unbekannt".
|
|
*/
|
|
export function tankstelleZeilen(
|
|
name: string | null | undefined,
|
|
adresse: string | null | undefined,
|
|
): { oben: string; unten: string } {
|
|
const t = tankstelleTeile(name, adresse)
|
|
const strasse = [t.strasse, t.hausnummer].filter(Boolean).join(" ")
|
|
let oben = [t.marke, strasse].filter(Boolean).join(", ")
|
|
let unten = [t.plz, t.ort].filter(Boolean).join(" ")
|
|
if (!oben && !unten) return { oben: t.roh || "unbekannt", unten: "" }
|
|
if (!oben) {
|
|
oben = unten
|
|
unten = ""
|
|
}
|
|
return { oben, unten }
|
|
}
|
|
|
|
/**
|
|
* Suchbegriffe für die Geokodierung, in der Reihenfolge, in der sie versucht
|
|
* werden: erst die Anschrift, dann der Name.
|
|
*
|
|
* NICHT beides zusammen. Am 04.09.2026 gemessen: `"Pascalstr. 8, 85057
|
|
* Ingolstadt"` findet Nominatim, `"A. Zrenner GmbH, Pascalstr. 8, 85057
|
|
* Ingolstadt"` nicht — der Betreibername steht in keiner Karte und lässt die
|
|
* Freitextsuche ins Leere laufen. Genau daran ist die erste Fassung dieser
|
|
* Funktion gescheitert, sichtbar als „Keine Position zur Tankstelle" bei einem
|
|
* Beleg, dessen Anschrift sich vorher hat auflösen lassen.
|
|
*
|
|
* Der Name allein ist trotzdem wertvoll: `"Shell, SÖLDEN"` findet Nominatim
|
|
* (46.9756/11.0111) — deshalb der zweite Versuch statt gar keinem.
|
|
*
|
|
* Und als dritter Versuch der Name OHNE die führende Marke. Seit dem 16.08.2026
|
|
* setzt der Belegparser den Namen als „Marke, Straße, Ort" zusammen; steht darin
|
|
* keine Postleitzahl, kippt die Marke die Suche. Am 04.09.2026 gemessen:
|
|
* `"Shell, Pascalstr. 8, Ingolstadt"` findet Nominatim NICHT,
|
|
* `"Pascalstr. 8, Ingolstadt"` dagegen schon. Genau daran blieb die Karte auf
|
|
* der realen Instanz leer, während dieselbe Tankstelle in der Testinstanz
|
|
* (dort steht die Anschrift getrennt daneben) gefunden wurde.
|
|
*/
|
|
export function tankstelleKandidaten(
|
|
name: string | null | undefined,
|
|
adresse: string | null | undefined,
|
|
): string[] {
|
|
const n = (name ?? "").trim()
|
|
const marke = markeErkennen(n)
|
|
// Nur abschneiden, wenn die Marke wirklich vorn steht und noch etwas folgt.
|
|
const ohneMarke =
|
|
marke && n.toLowerCase().startsWith(marke.toLowerCase())
|
|
? n.slice(marke.length).replace(/^[ ,]+/, "").trim()
|
|
: ""
|
|
const liste = [(adresse ?? "").trim(), n, ohneMarke].filter(Boolean)
|
|
return [...new Set(liste)]
|
|
}
|
|
|
|
/** Der erste Suchbegriff - leer heißt: der Beleg nennt gar nichts. */
|
|
export function tankstelleSuchtext(
|
|
name: string | null | undefined,
|
|
adresse: string | null | undefined,
|
|
): string {
|
|
return tankstelleKandidaten(name, adresse)[0] ?? ""
|
|
}
|
|
|
|
/**
|
|
* Das Ziel für den Kartendienst: Kopfmarke UND Anschrift. Apple Karten und
|
|
* Google Maps kennen Tankstellenmarken und treffen damit die Zapfsäule statt
|
|
* des Nachbargebäudes - beide sind darin deutlich besser als Nominatims
|
|
* Freitextsuche. Die Firmierung des Betreibers hilft dagegen nirgends; ohne
|
|
* erkennbare Marke geht die Anschrift allein hin.
|
|
*/
|
|
export function tankstelleZiel(
|
|
name: string | null | undefined,
|
|
adresse: string | null | undefined,
|
|
): string {
|
|
const a = (adresse ?? "").trim()
|
|
if (!a) return (name ?? "").trim()
|
|
return [markeErkennen(name) ?? "", a].filter(Boolean).join(", ")
|
|
}
|
|
|
|
/** Apple-Gerät? Dort ist Karten der Standarddienst. */
|
|
function apfelgeraet(): boolean {
|
|
if (typeof navigator === "undefined") return false
|
|
const kennung = `${navigator.userAgent || ""} ${navigator.platform || ""}`
|
|
return /iPhone|iPad|iPod|Macintosh|Mac OS X/i.test(kennung)
|
|
}
|
|
|
|
/**
|
|
* "route" berechnet den Weg dorthin, "zeigen" setzt nur eine Nadel. Der
|
|
* Tankstellen-Verweis im Einzelbeleg will das Zweite (Vorgabe des Eigentümers,
|
|
* 04.09.2026): erst sehen, wo die Tankstelle liegt - eine Route erwartet dort
|
|
* niemand, und sie ist auf dem Telefon nur einen weiteren Tipp entfernt.
|
|
*/
|
|
export type Kartenmodus = "route" | "zeigen"
|
|
|
|
/**
|
|
* Verweis in den Kartendienst - `null`, wenn nichts bekannt ist, wohin.
|
|
*
|
|
* Mit Koordinaten wird auf sie gezielt und der Name nur als Beschriftung
|
|
* mitgegeben; ohne sie muss die Anschrift als Suchtext genügen.
|
|
*/
|
|
export function kartendienstUrl(
|
|
name: string | null | undefined,
|
|
adresse: string | null | undefined,
|
|
pos?: Koordinate | null,
|
|
modus: Kartenmodus = "route",
|
|
): string | null {
|
|
const beschriftung = (name ?? "").trim()
|
|
const ziel = pos ? `${pos.lat},${pos.lon}` : tankstelleZiel(name, adresse)
|
|
if (!ziel) return null
|
|
if (apfelgeraet()) {
|
|
if (modus === "zeigen") {
|
|
// Mit Koordinaten der Punkt selbst, sonst der Suchtext - beides zeigt
|
|
// Karten nur an, ohne eine Route zu berechnen.
|
|
return pos
|
|
? `https://maps.apple.com/?ll=${ziel}&q=${encodeURIComponent(beschriftung || "Ziel")}`
|
|
: `https://maps.apple.com/?q=${encodeURIComponent(ziel)}`
|
|
}
|
|
const q = pos && beschriftung ? `&q=${encodeURIComponent(beschriftung)}` : ""
|
|
return `https://maps.apple.com/?daddr=${encodeURIComponent(ziel)}${q}`
|
|
}
|
|
return modus === "zeigen"
|
|
? `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(ziel)}`
|
|
: `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(ziel)}`
|
|
}
|
|
|
|
/** Wie oben, aber für einen reinen Punkt (Fahrzeugstandort, Autohaus). */
|
|
export function kartendienstUrlZuPunkt(pos: Koordinate | null | undefined): string | null {
|
|
if (!pos) return null
|
|
return kartendienstUrl(null, null, pos)
|
|
}
|