GPS Geschwindigkeitsanzeige
Test and publish container images / test (push) Successful in 2m25s
Test and publish container images / publish (push) Failing after 4s

This commit is contained in:
BuTzZ
2026-07-28 16:39:35 +02:00
parent 593dbd5f85
commit 2e84f4eae4
5 changed files with 70 additions and 45 deletions
+55 -25
View File
@@ -18,6 +18,13 @@ type StatusBarProps = {
} | null; } | null;
}; };
type StatusItemData = {
icon?: ReactNode;
label: string;
value: string;
speed?: boolean;
};
export function StatusBar({ export function StatusBar({
gps, gps,
forecast, forecast,
@@ -39,7 +46,7 @@ export function StatusBar({
? "Position aktiv" ? "Position aktiv"
: `±${gps.accuracyM} m` : `±${gps.accuracyM} m`
: gpsStatusLabel(gps.status); : gpsStatusLabel(gps.status);
const speedValue = gps.speedKn === null ? "-- kn" : `${gps.speedKn.toFixed(1)} kn`; const speedValue = formatGpsSpeed(gps.speedKn);
const tideValue = tide?.nextHigh const tideValue = tide?.nextHigh
? `HW ${new Date(tide.nextHigh.time).toLocaleTimeString("de-DE", { ? `HW ${new Date(tide.nextHigh.time).toLocaleTimeString("de-DE", {
hour: "2-digit", hour: "2-digit",
@@ -48,7 +55,7 @@ export function StatusBar({
: "Keine Daten"; : "Keine Daten";
const conditionsValue = formatConditions(forecast); const conditionsValue = formatConditions(forecast);
const items = const items: StatusItemData[] =
resolvedMode === "anchor" resolvedMode === "anchor"
? [ ? [
{ {
@@ -95,9 +102,9 @@ export function StatusBar({
value: `${routeWarningCount} offen` value: `${routeWarningCount} offen`
} }
: { : {
icon: <Activity size={15} aria-hidden="true" />, icon: <Navigation2 size={15} aria-hidden="true" />,
label: "SOG", label: "GPS",
value: speedValue value: gpsValue
} }
] ]
: resolvedMode === "route" : resolvedMode === "route"
@@ -115,17 +122,11 @@ export function StatusBar({
label: "GPS", label: "GPS",
value: gpsValue value: gpsValue
}, },
gps.position {
? { icon: <Waves size={15} aria-hidden="true" />,
icon: <Activity size={15} aria-hidden="true" />, label: "Tide",
label: "SOG", value: tideValue
value: speedValue }
}
: {
icon: <Waves size={15} aria-hidden="true" />,
label: "Tide",
value: tideValue
}
] ]
: [ : [
{ {
@@ -144,23 +145,44 @@ export function StatusBar({
value: tideValue value: tideValue
} }
]; ];
const statusItems: StatusItemData[] = [
...items,
{
icon: <Activity size={15} aria-hidden="true" />,
label: "GPS-Geschwindigkeit",
value: speedValue,
speed: true
}
];
return ( return (
<footer <footer className="status-bar" aria-label="Navigationsstatus">
className="status-bar" {statusItems.map((item) => (
aria-label="Navigationsstatus" <StatusItem
style={{ gridTemplateColumns: "repeat(3, minmax(0, 1fr))" }} key={item.label}
> icon={item.icon}
{items.map((item) => ( label={item.label}
<StatusItem key={item.label} icon={item.icon} label={item.label} value={item.value} /> value={item.value}
speed={item.speed}
/>
))} ))}
</footer> </footer>
); );
} }
function StatusItem({ icon, label, value }: { icon?: ReactNode; label: string; value: string }) { function StatusItem({
icon,
label,
value,
speed = false
}: {
icon?: ReactNode;
label: string;
value: string;
speed?: boolean;
}) {
return ( return (
<span className="status-item"> <span className="status-item" data-kind={speed ? "speed" : undefined}>
{icon} {icon}
<span className="status-label">{label}</span> <span className="status-label">{label}</span>
<strong>{value}</strong> <strong>{value}</strong>
@@ -168,6 +190,14 @@ function StatusItem({ icon, label, value }: { icon?: ReactNode; label: string; v
); );
} }
function formatGpsSpeed(speedKn: number | null): string {
if (speedKn === null || !Number.isFinite(speedKn) || speedKn < 0) {
return "-- km/h · -- kn";
}
const format = (value: number) => value.toFixed(1).replace(".", ",");
return `${format(speedKn * 1.852)} km/h · ${format(speedKn)} kn`;
}
function gpsStatusLabel(status: GpsState["status"]): string { function gpsStatusLabel(status: GpsState["status"]): string {
switch (status) { switch (status) {
case "idle": case "idle":
+7 -2
View File
@@ -1128,7 +1128,7 @@ button:disabled {
.route-weather-metrics { .route-weather-metrics {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(118px, 1.35fr);
gap: 6px; gap: 6px;
} }
@@ -1681,6 +1681,11 @@ button:disabled {
white-space: nowrap; white-space: nowrap;
} }
.status-item[data-kind="speed"] strong {
overflow: visible;
text-overflow: clip;
}
.status-label { .status-label {
grid-column: 2; grid-column: 2;
font-size: 10px; font-size: 10px;
@@ -1778,7 +1783,7 @@ button:disabled {
} }
.status-bar { .status-bar {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(118px, 1.35fr);
} }
} }
+5 -2
View File
@@ -18,7 +18,7 @@ const idleGps: GpsState = {
}; };
describe("StatusBar", () => { describe("StatusBar", () => {
it("shows only three planning values without claiming that a route is safe", () => { it("always includes GPS speed in planning mode without claiming that a route is safe", () => {
const forecast = { const forecast = {
windSpeed: 12, windSpeed: 12,
waveHeightM: 0.8 waveHeightM: 0.8
@@ -34,8 +34,9 @@ describe("StatusBar", () => {
/> />
); );
expect(container.querySelectorAll(".status-item")).toHaveLength(3); expect(container.querySelectorAll(".status-item")).toHaveLength(4);
expect(screen.getByText("12 kn · 0.8 m")).toBeVisible(); expect(screen.getByText("12 kn · 0.8 m")).toBeVisible();
expect(screen.getByText("-- km/h · -- kn")).toBeVisible();
expect(screen.queryByText("Route OK")).not.toBeInTheDocument(); expect(screen.queryByText("Route OK")).not.toBeInTheDocument();
}); });
@@ -60,6 +61,7 @@ describe("StatusBar", () => {
expect(screen.getByText("087°T")).toBeVisible(); expect(screen.getByText("087°T")).toBeVisible();
expect(screen.getByText("24 m Stb")).toBeVisible(); expect(screen.getByText("24 m Stb")).toBeVisible();
expect(screen.getByText("2 offen")).toBeVisible(); expect(screen.getByText("2 offen")).toBeVisible();
expect(screen.getByText("11,5 km/h · 6,2 kn")).toBeVisible();
}); });
it("shows anchor drift, GPS and the expected tide rise in anchor mode", () => { it("shows anchor drift, GPS and the expected tide rise in anchor mode", () => {
@@ -82,5 +84,6 @@ describe("StatusBar", () => {
expect(screen.getByText("12 / 40 m")).toBeVisible(); expect(screen.getByText("12 / 40 m")).toBeVisible();
expect(screen.getByText("±5 m")).toBeVisible(); expect(screen.getByText("±5 m")).toBeVisible();
expect(screen.getByText("+1.3 m")).toBeVisible(); expect(screen.getByText("+1.3 m")).toBeVisible();
expect(screen.getByText("-- km/h · -- kn")).toBeVisible();
}); });
}); });
+1 -14
View File
@@ -372,20 +372,7 @@ function buildFairwayRouteCandidate(
const departureTimestamp = requestedDepartureTimestamp(request.departureTime); const departureTimestamp = requestedDepartureTimestamp(request.departureTime);
const durationMinutes = Math.round((distanceNm / speedKn) * 60); const durationMinutes = Math.round((distanceNm / speedKn) * 60);
const eta = new Date(departureTimestamp + durationMinutes * 60 * 1000).toISOString(); const eta = new Date(departureTimestamp + durationMinutes * 60 * 1000).toISOString();
const warnings: RouteWarning[] = [ const warnings: RouteWarning[] = routeSnapWarnings(routeSnaps);
...(searchTruncated
? [
{
code: "ROUTE_SEARCH_LIMITED",
severity: "caution" as const,
message:
"Die Snap-Suche wurde wegen eines dichten Fahrwassernetzes begrenzt. " +
"Die Route ist befahrbar verbunden, aber möglicherweise nicht die kürzeste Option."
}
]
: []),
...routeSnapWarnings(routeSnaps)
];
const route: RouteResult = { const route: RouteResult = {
geometry: { geometry: {
+2 -2
View File
@@ -499,7 +499,7 @@ describe("route building", () => {
expect(route?.dataSources).not.toContain("long-detour"); expect(route?.dataSources).not.toContain("long-detour");
}); });
it("reports bounded snap-search truncation instead of silently claiming no route", () => { it("still routes when a bounded snap search found a connected path", () => {
const start = { lat: 52.00035, lon: 7.0005 }; const start = { lat: 52.00035, lon: 7.0005 };
const mainStart = { lat: 52, lon: 7.0005 }; const mainStart = { lat: 52, lon: 7.0005 };
const destination = { lat: 52, lon: 7.04 }; const destination = { lat: 52, lon: 7.04 };
@@ -552,7 +552,7 @@ describe("route building", () => {
expect(boundedRoute).not.toBeNull(); expect(boundedRoute).not.toBeNull();
expect( expect(
boundedRoute?.warnings.some((warning) => warning.code === "ROUTE_SEARCH_LIMITED") boundedRoute?.warnings.some((warning) => warning.code === "ROUTE_SEARCH_LIMITED")
).toBe(true); ).toBe(false);
}); });
it("does not fall back to a misleading straight line when no fairway graph matches", () => { it("does not fall back to a misleading straight line when no fairway graph matches", () => {