GPS Geschwindigkeitsanzeige
This commit is contained in:
@@ -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,13 +122,7 @@ export function StatusBar({
|
|||||||
label: "GPS",
|
label: "GPS",
|
||||||
value: gpsValue
|
value: gpsValue
|
||||||
},
|
},
|
||||||
gps.position
|
{
|
||||||
? {
|
|
||||||
icon: <Activity size={15} aria-hidden="true" />,
|
|
||||||
label: "SOG",
|
|
||||||
value: speedValue
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
icon: <Waves size={15} aria-hidden="true" />,
|
icon: <Waves size={15} aria-hidden="true" />,
|
||||||
label: "Tide",
|
label: "Tide",
|
||||||
value: tideValue
|
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":
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user