GPS Geschwindigkeitsanzeige
This commit is contained in:
@@ -18,6 +18,13 @@ type StatusBarProps = {
|
||||
} | null;
|
||||
};
|
||||
|
||||
type StatusItemData = {
|
||||
icon?: ReactNode;
|
||||
label: string;
|
||||
value: string;
|
||||
speed?: boolean;
|
||||
};
|
||||
|
||||
export function StatusBar({
|
||||
gps,
|
||||
forecast,
|
||||
@@ -39,7 +46,7 @@ export function StatusBar({
|
||||
? "Position aktiv"
|
||||
: `±${gps.accuracyM} m`
|
||||
: gpsStatusLabel(gps.status);
|
||||
const speedValue = gps.speedKn === null ? "-- kn" : `${gps.speedKn.toFixed(1)} kn`;
|
||||
const speedValue = formatGpsSpeed(gps.speedKn);
|
||||
const tideValue = tide?.nextHigh
|
||||
? `HW ${new Date(tide.nextHigh.time).toLocaleTimeString("de-DE", {
|
||||
hour: "2-digit",
|
||||
@@ -48,7 +55,7 @@ export function StatusBar({
|
||||
: "Keine Daten";
|
||||
const conditionsValue = formatConditions(forecast);
|
||||
|
||||
const items =
|
||||
const items: StatusItemData[] =
|
||||
resolvedMode === "anchor"
|
||||
? [
|
||||
{
|
||||
@@ -95,9 +102,9 @@ export function StatusBar({
|
||||
value: `${routeWarningCount} offen`
|
||||
}
|
||||
: {
|
||||
icon: <Activity size={15} aria-hidden="true" />,
|
||||
label: "SOG",
|
||||
value: speedValue
|
||||
icon: <Navigation2 size={15} aria-hidden="true" />,
|
||||
label: "GPS",
|
||||
value: gpsValue
|
||||
}
|
||||
]
|
||||
: resolvedMode === "route"
|
||||
@@ -115,17 +122,11 @@ export function StatusBar({
|
||||
label: "GPS",
|
||||
value: gpsValue
|
||||
},
|
||||
gps.position
|
||||
? {
|
||||
icon: <Activity size={15} aria-hidden="true" />,
|
||||
label: "SOG",
|
||||
value: speedValue
|
||||
}
|
||||
: {
|
||||
icon: <Waves size={15} aria-hidden="true" />,
|
||||
label: "Tide",
|
||||
value: tideValue
|
||||
}
|
||||
{
|
||||
icon: <Waves size={15} aria-hidden="true" />,
|
||||
label: "Tide",
|
||||
value: tideValue
|
||||
}
|
||||
]
|
||||
: [
|
||||
{
|
||||
@@ -144,23 +145,44 @@ export function StatusBar({
|
||||
value: tideValue
|
||||
}
|
||||
];
|
||||
const statusItems: StatusItemData[] = [
|
||||
...items,
|
||||
{
|
||||
icon: <Activity size={15} aria-hidden="true" />,
|
||||
label: "GPS-Geschwindigkeit",
|
||||
value: speedValue,
|
||||
speed: true
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<footer
|
||||
className="status-bar"
|
||||
aria-label="Navigationsstatus"
|
||||
style={{ gridTemplateColumns: "repeat(3, minmax(0, 1fr))" }}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<StatusItem key={item.label} icon={item.icon} label={item.label} value={item.value} />
|
||||
<footer className="status-bar" aria-label="Navigationsstatus">
|
||||
{statusItems.map((item) => (
|
||||
<StatusItem
|
||||
key={item.label}
|
||||
icon={item.icon}
|
||||
label={item.label}
|
||||
value={item.value}
|
||||
speed={item.speed}
|
||||
/>
|
||||
))}
|
||||
</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 (
|
||||
<span className="status-item">
|
||||
<span className="status-item" data-kind={speed ? "speed" : undefined}>
|
||||
{icon}
|
||||
<span className="status-label">{label}</span>
|
||||
<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 {
|
||||
switch (status) {
|
||||
case "idle":
|
||||
|
||||
@@ -1128,7 +1128,7 @@ button:disabled {
|
||||
|
||||
.route-weather-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(118px, 1.35fr);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
@@ -1681,6 +1681,11 @@ button:disabled {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-item[data-kind="speed"] strong {
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
grid-column: 2;
|
||||
font-size: 10px;
|
||||
@@ -1778,7 +1783,7 @@ button:disabled {
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(118px, 1.35fr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const idleGps: GpsState = {
|
||||
};
|
||||
|
||||
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 = {
|
||||
windSpeed: 12,
|
||||
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("-- km/h · -- kn")).toBeVisible();
|
||||
expect(screen.queryByText("Route OK")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -60,6 +61,7 @@ describe("StatusBar", () => {
|
||||
expect(screen.getByText("087°T")).toBeVisible();
|
||||
expect(screen.getByText("24 m Stb")).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", () => {
|
||||
@@ -82,5 +84,6 @@ describe("StatusBar", () => {
|
||||
expect(screen.getByText("12 / 40 m")).toBeVisible();
|
||||
expect(screen.getByText("±5 m")).toBeVisible();
|
||||
expect(screen.getByText("+1.3 m")).toBeVisible();
|
||||
expect(screen.getByText("-- km/h · -- kn")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -372,20 +372,7 @@ function buildFairwayRouteCandidate(
|
||||
const departureTimestamp = requestedDepartureTimestamp(request.departureTime);
|
||||
const durationMinutes = Math.round((distanceNm / speedKn) * 60);
|
||||
const eta = new Date(departureTimestamp + durationMinutes * 60 * 1000).toISOString();
|
||||
const warnings: RouteWarning[] = [
|
||||
...(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 warnings: RouteWarning[] = routeSnapWarnings(routeSnaps);
|
||||
|
||||
const route: RouteResult = {
|
||||
geometry: {
|
||||
|
||||
@@ -499,7 +499,7 @@ describe("route building", () => {
|
||||
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 mainStart = { lat: 52, lon: 7.0005 };
|
||||
const destination = { lat: 52, lon: 7.04 };
|
||||
@@ -552,7 +552,7 @@ describe("route building", () => {
|
||||
expect(boundedRoute).not.toBeNull();
|
||||
expect(
|
||||
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", () => {
|
||||
|
||||
Reference in New Issue
Block a user