Batteriespannung: 8V-Plausibilitätsgrenze; Fahrten: echte Start-/Zielposition und Streckenlinie
Batteriespannung: Werte unter 8V (Sensor-/Verbindungsfehler, nicht physikalisch
plausibel für eine 12V-Bleibatterie) werden nicht mehr erfasst - live und beim
Import. "Neue Räder anlegen"-Knopf sitzt jetzt auf derselben Zeile wie "Montiert".
Fahrten: start_lat/-lon, end_lat/-lon und route wurden bisher nur beim
rückwirkenden Import befüllt, nie bei live erkannten Fahrten (dem Normalfall) -
Screening trägt sie jetzt aus dem GPS-Verlauf nach, unabhängig vom
Kilometerstand-Status. fakeTrack() (eine erfundene Linie) ist entfernt, die
Karte zeichnet jetzt die echte Route oder eine ehrliche Gerade zwischen den
bekannten Punkten. CARTOs anonyme Kartenkacheln verlangen inzwischen einen
API-Schlüssel ("API key required" auf jeder Karte in beiden Apps) - auf
schlüssellose OpenStreetMap-Kacheln umgestellt. Start/Ziel zeigen jetzt Datum
und Uhrzeit über der Adresse; die fehlerhafte "Status"-Zeile ist entfernt.
Batteriespannungs-Diagramm nutzt auf großen Bildschirmen die volle
Spaltenbreite statt einer festen 400px-Deckelung.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,10 +4,10 @@
|
||||
Die Fachtypen (Fahrt, Tankvorgang, ...) sind aus SPECIFICATION.md §5
|
||||
"Data files" abgeleitet - also aus dem, was das Backend tatsächlich in
|
||||
fahrten.jsonl / tankvorgaenge.jsonl schreibt, nicht aus Wunschdenken.
|
||||
Felder, die laut §7 im Schema stehen, aber von keinem Codepfad je gefüllt
|
||||
werden (start_lat, avg_speed_kmh, route, ...), sind hier bewusst als
|
||||
optional markiert - sie können null/undefined sein und die Oberfläche darf
|
||||
sich nie darauf verlassen. */
|
||||
Mehrere Felder (start_lat/-lon, end_lat/-lon, route, avg_speed_kmh) sind
|
||||
nur befüllt, wenn ein GPS-Sensor zugeordnet ist (screening.py/
|
||||
historienimport.py) - ohne Zuordnung bleiben sie null. Deshalb hier bewusst
|
||||
optional markiert; die Oberfläche darf sich nie auf ihre Existenz verlassen. */
|
||||
|
||||
/** Roher Zustand einer HA-Entität, wie ihn /api/states liefert. */
|
||||
export interface HassState<A = Record<string, unknown>> {
|
||||
@@ -55,7 +55,7 @@ export interface Fahrt {
|
||||
end_lon?: number | null;
|
||||
start_address?: string | null;
|
||||
end_address?: string | null;
|
||||
route?: unknown | null;
|
||||
route?: [number, number][] | null;
|
||||
pausen?: unknown[];
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@ export function FahrtDetail({ id }: { id: string | undefined }) {
|
||||
{...(fahrt.end_lat != null && fahrt.end_lon != null
|
||||
? { ziel: { lat: fahrt.end_lat, lon: fahrt.end_lon } }
|
||||
: {})}
|
||||
{...(fahrt.route && fahrt.route.length >= 2 ? { route: fahrt.route } : {})}
|
||||
/>
|
||||
) : (
|
||||
<Leerzustand
|
||||
@@ -117,16 +118,22 @@ export function FahrtDetail({ id }: { id: string | undefined }) {
|
||||
text="Das Fahrzeug liefert derzeit keine Positionsdaten. Sobald es das tut, erscheint hier die gefahrene Strecke auf der Karte."
|
||||
/>
|
||||
)}
|
||||
{(fahrt.start_address || fahrt.end_address) && (
|
||||
<Werteliste
|
||||
kinder={
|
||||
<>
|
||||
<Wertzeile label="Start" wert={fahrt.start_address ?? "—"} />
|
||||
<Wertzeile label="Ziel" wert={fahrt.end_address ?? "—"} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Werteliste
|
||||
kinder={
|
||||
<>
|
||||
<Wertzeile
|
||||
label="Start"
|
||||
wert={`${datum(fahrt.ts_start)} · ${uhrzeit(fahrt.ts_start)} Uhr`}
|
||||
zusatz={fahrt.start_address ?? "unbekannt"}
|
||||
/>
|
||||
<Wertzeile
|
||||
label="Ziel"
|
||||
wert={`${datum(fahrt.ts_end)} · ${uhrzeit(fahrt.ts_end)} Uhr`}
|
||||
zusatz={fahrt.end_address ?? "unbekannt"}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Tile>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -13,27 +13,30 @@ import { useEffect, useRef } from "react"
|
||||
|
||||
import "leaflet/dist/leaflet.css"
|
||||
|
||||
import { useTheme } from "../theme"
|
||||
|
||||
export interface Punkt {
|
||||
lat: number
|
||||
lon: number
|
||||
}
|
||||
|
||||
const KACHELN = {
|
||||
nacht: {
|
||||
url: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png",
|
||||
zusatz: "© OpenStreetMap, © CARTO",
|
||||
},
|
||||
tag: {
|
||||
url: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png",
|
||||
zusatz: "© OpenStreetMap, © CARTO",
|
||||
},
|
||||
} as const
|
||||
// Themeunabhaengig hell wie im Panel (Kartendienste zeigen die
|
||||
// Standard-Strassenkarte immer hell) - und seit CARTOs anonyme
|
||||
// basemaps.cartocdn.com-Kacheln einen eigenen API-Schluessel verlangen
|
||||
// (ohne ihn nur noch "API key required"-Kacheln), die einzige hier weiterhin
|
||||
// ohne Schluessel nutzbare Quelle.
|
||||
const KACHEL_URL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
const KACHEL_ZUSATZ = "© OpenStreetMap contributors"
|
||||
|
||||
export function Karte({ start, ziel }: { start: Punkt; ziel?: Punkt }) {
|
||||
export function Karte({
|
||||
start,
|
||||
ziel,
|
||||
route,
|
||||
}: {
|
||||
start: Punkt
|
||||
ziel?: Punkt
|
||||
/** Echte Streckenlinie aus dem GPS-Verlauf (Backend: screening.py/historienimport.py). */
|
||||
route?: [number, number][]
|
||||
}) {
|
||||
const behaelter = useRef<HTMLDivElement | null>(null)
|
||||
const [theme] = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
const element = behaelter.current
|
||||
@@ -47,8 +50,7 @@ export function Karte({ start, ziel }: { start: Punkt; ziel?: Punkt }) {
|
||||
void import("leaflet").then((L) => {
|
||||
if (abgebrochen) return
|
||||
karte = L.map(element, { attributionControl: true, scrollWheelZoom: false })
|
||||
const kachel = KACHELN[theme]
|
||||
L.tileLayer(kachel.url, { attribution: kachel.zusatz, maxZoom: 19 }).addTo(karte)
|
||||
L.tileLayer(KACHEL_URL, { attribution: KACHEL_ZUSATZ, maxZoom: 19 }).addTo(karte)
|
||||
|
||||
const punkte: [number, number][] = [[start.lat, start.lon]]
|
||||
L.circleMarker([start.lat, start.lon], { radius: 6, color: "#F50537" })
|
||||
@@ -60,8 +62,11 @@ export function Karte({ start, ziel }: { start: Punkt; ziel?: Punkt }) {
|
||||
L.circleMarker([ziel.lat, ziel.lon], { radius: 6, color: "#F50537", fill: true })
|
||||
.bindTooltip("Ziel")
|
||||
.addTo(karte)
|
||||
// Verbindungslinie NUR zwischen echten Punkten, kein erfundener Verlauf.
|
||||
L.polyline(punkte, { color: "#F50537", weight: 2, dashArray: "4 6" }).addTo(karte)
|
||||
// Echte Route, wenn vorhanden - sonst eine Verbindungslinie NUR zwischen
|
||||
// den beiden echten Punkten, kein erfundener Verlauf dazwischen.
|
||||
const linie = route && route.length >= 2 ? route : punkte
|
||||
L.polyline(linie, { color: "#F50537", weight: 2, dashArray: route && route.length >= 2 ? undefined : "4 6" }).addTo(karte)
|
||||
if (route && route.length >= 2) punkte.push(...route)
|
||||
}
|
||||
|
||||
if (punkte.length > 1) {
|
||||
@@ -75,7 +80,7 @@ export function Karte({ start, ziel }: { start: Punkt; ziel?: Punkt }) {
|
||||
abgebrochen = true
|
||||
karte?.remove()
|
||||
}
|
||||
}, [start, ziel, theme])
|
||||
}, [start, ziel, route])
|
||||
|
||||
return <div ref={behaelter} className="dm-karte" role="img" aria-label="Karte der Fahrt" />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user