Verschiebemodus eingebaut
Test and publish container images / test (push) Successful in 2m16s
Test and publish container images / publish (push) Successful in 1m6s

This commit is contained in:
BuTzZ
2026-08-03 08:31:45 +02:00
parent 6801fa9089
commit 5994d1e71c
4 changed files with 142 additions and 5 deletions
+9
View File
@@ -155,6 +155,7 @@ export function App() {
const [routeLoading, setRouteLoading] = useState(false);
const [routeError, setRouteError] = useState<string | null>(null);
const [mapReady, setMapReady] = useState(false);
const [followGpsPosition, setFollowGpsPosition] = useState(false);
const [mapFocusRequest, setMapFocusRequest] = useState<{
key: string;
coordinate: Coordinate;
@@ -273,6 +274,11 @@ export function App() {
setActiveTool("route");
setWorkspaceSheetState("half");
}, [anchorWatch.phase, compass, courseAssistant, gps, route]);
useEffect(() => {
if (!courseAssistant.active) {
setFollowGpsPosition(false);
}
}, [courseAssistant.active]);
const anchorPanelVisible = activeTool === "anchor";
const closeWorkspace = useCallback(() => setActiveTool(null), []);
const selectTool = useCallback((tool: NavigationToolId) => {
@@ -830,8 +836,11 @@ export function App() {
waypoints={waypoints}
pickMode={pickMode}
route={route}
navigationActive={courseAssistant.active}
guidanceActive={courseAssistant.active && Boolean(activeGuidance)}
guidanceTarget={activeGuidance?.lookaheadPoint ?? null}
followPosition={followGpsPosition}
onFollowPositionChange={setFollowGpsPosition}
anchorPoint={anchorWatch.anchorPoint}
anchorAlarmRadiusM={anchorWatch.anchorPoint ? anchorWatch.settings.alarmRadiusM : null}
anchorWatchActive={anchorWatch.phase === "armed"}
+62 -5
View File
@@ -16,8 +16,11 @@ type MapViewProps = {
waypoints?: Coordinate[];
pickMode: "start" | "destination" | "waypoint" | null;
route: RouteResult | null;
navigationActive?: boolean;
guidanceActive?: boolean;
guidanceTarget?: Coordinate | null;
followPosition?: boolean;
onFollowPositionChange?: (follow: boolean) => void;
anchorPoint?: Coordinate | null;
anchorAlarmRadiusM?: number | null;
anchorWatchActive?: boolean;
@@ -71,8 +74,11 @@ export function MapView({
waypoints = [],
pickMode,
route,
navigationActive = false,
guidanceActive = false,
guidanceTarget = null,
followPosition = false,
onFollowPositionChange,
anchorPoint = null,
anchorAlarmRadiusM = null,
anchorWatchActive = false,
@@ -98,6 +104,7 @@ export function MapView({
const accuracyMRef = useRef(accuracyM);
const guidanceActiveRef = useRef(guidanceActive);
const guidanceTargetRef = useRef(guidanceTarget);
const onFollowPositionChangeRef = useRef(onFollowPositionChange);
const anchorPointRef = useRef(anchorPoint);
const anchorAlarmRadiusMRef = useRef(anchorAlarmRadiusM);
const anchorWatchActiveRef = useRef(anchorWatchActive);
@@ -114,6 +121,10 @@ export function MapView({
applyMapFocusRequest(mapRef.current, focusRequest);
}, [focusRequest]);
useEffect(() => {
onFollowPositionChangeRef.current = onFollowPositionChange;
}, [onFollowPositionChange]);
const clearContactFeatures = useCallback((map: MapLibreMap) => {
const hadContactFeatures = contactFeaturesRef.current.length > 0;
contactFeaturesRef.current = [];
@@ -174,6 +185,9 @@ export function MapView({
map.addControl(new maplibregl.AttributionControl({ compact: true }), "bottom-right");
map.addControl(new maplibregl.ScaleControl({ unit: "nautical" }), "bottom-left");
map.on("dragstart", () => {
onFollowPositionChangeRef.current?.(false);
});
mapRef.current = map;
map.on("load", () => {
@@ -937,7 +951,15 @@ export function MapView({
const source = map.getSource("user-position") as GeoJSONSource | undefined;
source?.setData(userPositionFeatures(position, accuracyM));
if (!hasCenteredOnFirstFixRef.current) {
if (followPosition) {
hasCenteredOnFirstFixRef.current = true;
map.easeTo({
center: [position.lon, position.lat],
zoom: Math.max(map.getZoom(), 13),
duration: 500,
essential: true
});
} else if (!hasCenteredOnFirstFixRef.current) {
hasCenteredOnFirstFixRef.current = true;
map.easeTo({
center: [position.lon, position.lat],
@@ -946,7 +968,7 @@ export function MapView({
essential: true
});
}
}, [accuracyM, position]);
}, [accuracyM, followPosition, position]);
useEffect(() => {
const map = mapRef.current;
@@ -1075,6 +1097,7 @@ export function MapView({
data-pick-mode={pickMode ?? "none"}
data-has-route={Boolean(route)}
data-guidance-active={guidanceActive}
data-follow-position={followPosition}
>
<div ref={containerRef} className="map-container" data-testid="map-container" />
{pickMode && (
@@ -1101,9 +1124,43 @@ export function MapView({
<button
className="map-tool locate-toggle"
type="button"
title="Position zentrieren"
aria-label="Position zentrieren"
onClick={() => mapRef.current?.flyTo({ center: [position.lon, position.lat], zoom: 13 })}
title={
navigationActive
? followPosition
? "GPS-Folge-Modus beenden"
: "GPS-Folge-Modus aktivieren"
: "Position zentrieren"
}
aria-label={
navigationActive
? followPosition
? "GPS-Folge-Modus beenden"
: "GPS-Folge-Modus aktivieren"
: "Position zentrieren"
}
aria-pressed={navigationActive ? followPosition : undefined}
data-active={navigationActive && followPosition}
onClick={() => {
if (navigationActive && onFollowPositionChange) {
const next = !followPosition;
onFollowPositionChange(next);
if (!next) {
return;
}
}
if (navigationActive) {
mapRef.current?.flyTo({
center: [position.lon, position.lat],
zoom: Math.max(mapRef.current?.getZoom() ?? 13, 13),
essential: true
});
} else {
mapRef.current?.flyTo({
center: [position.lon, position.lat],
zoom: 13
});
}
}}
>
<LocateFixed size={18} aria-hidden="true" />
</button>
+6
View File
@@ -455,6 +455,12 @@ button:disabled {
right: 62px;
}
.locate-toggle[data-active="true"] {
background: #0f4c5c;
color: #ffffff;
border-color: rgba(255, 255, 255, 0.55);
}
.anchor-toggle {
z-index: 10;
top: calc(env(safe-area-inset-top) + 116px);
+65
View File
@@ -788,6 +788,71 @@ describe("MapView marine feature information", () => {
);
expect(maplibreState.markerConstructions).toBe(0);
});
it("keeps the GPS position centered while navigation follow mode is active", async () => {
const onFollowPositionChange = vi.fn();
const onMapReady = vi.fn();
const onPickCoordinate = vi.fn();
const view = render(
<MapView
config={config}
position={{ lat: 52, lon: 7 }}
accuracyM={5}
startPoint={null}
destination={null}
pickMode={null}
route={null}
navigationActive
followPosition={false}
onFollowPositionChange={onFollowPositionChange}
onPickCoordinate={onPickCoordinate}
onMapReady={onMapReady}
/>
);
await act(async () => maplibreState.current.emit("load"));
const followButton = screen.getByRole("button", {
name: "GPS-Folge-Modus aktivieren"
});
expect(followButton).toHaveAttribute("aria-pressed", "false");
fireEvent.click(followButton);
expect(onFollowPositionChange).toHaveBeenCalledWith(true);
expect(maplibreState.current.flyTo).toHaveBeenCalledWith({
center: [7, 52],
zoom: 13,
essential: true
});
view.rerender(
<MapView
config={config}
position={{ lat: 52.01, lon: 7.02 }}
accuracyM={5}
startPoint={null}
destination={null}
pickMode={null}
route={null}
navigationActive
followPosition
onFollowPositionChange={onFollowPositionChange}
onPickCoordinate={onPickCoordinate}
onMapReady={onMapReady}
/>
);
expect(
screen.getByRole("button", { name: "GPS-Folge-Modus beenden" })
).toHaveAttribute("aria-pressed", "true");
expect(maplibreState.current.easeTo).toHaveBeenLastCalledWith({
center: [7.02, 52.01],
zoom: 13,
duration: 500,
essential: true
});
await act(async () => maplibreState.current.emit("dragstart"));
expect(onFollowPositionChange).toHaveBeenLastCalledWith(false);
});
});
describe("MapView anchor watch", () => {