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 [routeLoading, setRouteLoading] = useState(false);
const [routeError, setRouteError] = useState<string | null>(null); const [routeError, setRouteError] = useState<string | null>(null);
const [mapReady, setMapReady] = useState(false); const [mapReady, setMapReady] = useState(false);
const [followGpsPosition, setFollowGpsPosition] = useState(false);
const [mapFocusRequest, setMapFocusRequest] = useState<{ const [mapFocusRequest, setMapFocusRequest] = useState<{
key: string; key: string;
coordinate: Coordinate; coordinate: Coordinate;
@@ -273,6 +274,11 @@ export function App() {
setActiveTool("route"); setActiveTool("route");
setWorkspaceSheetState("half"); setWorkspaceSheetState("half");
}, [anchorWatch.phase, compass, courseAssistant, gps, route]); }, [anchorWatch.phase, compass, courseAssistant, gps, route]);
useEffect(() => {
if (!courseAssistant.active) {
setFollowGpsPosition(false);
}
}, [courseAssistant.active]);
const anchorPanelVisible = activeTool === "anchor"; const anchorPanelVisible = activeTool === "anchor";
const closeWorkspace = useCallback(() => setActiveTool(null), []); const closeWorkspace = useCallback(() => setActiveTool(null), []);
const selectTool = useCallback((tool: NavigationToolId) => { const selectTool = useCallback((tool: NavigationToolId) => {
@@ -830,8 +836,11 @@ export function App() {
waypoints={waypoints} waypoints={waypoints}
pickMode={pickMode} pickMode={pickMode}
route={route} route={route}
navigationActive={courseAssistant.active}
guidanceActive={courseAssistant.active && Boolean(activeGuidance)} guidanceActive={courseAssistant.active && Boolean(activeGuidance)}
guidanceTarget={activeGuidance?.lookaheadPoint ?? null} guidanceTarget={activeGuidance?.lookaheadPoint ?? null}
followPosition={followGpsPosition}
onFollowPositionChange={setFollowGpsPosition}
anchorPoint={anchorWatch.anchorPoint} anchorPoint={anchorWatch.anchorPoint}
anchorAlarmRadiusM={anchorWatch.anchorPoint ? anchorWatch.settings.alarmRadiusM : null} anchorAlarmRadiusM={anchorWatch.anchorPoint ? anchorWatch.settings.alarmRadiusM : null}
anchorWatchActive={anchorWatch.phase === "armed"} anchorWatchActive={anchorWatch.phase === "armed"}
+62 -5
View File
@@ -16,8 +16,11 @@ type MapViewProps = {
waypoints?: Coordinate[]; waypoints?: Coordinate[];
pickMode: "start" | "destination" | "waypoint" | null; pickMode: "start" | "destination" | "waypoint" | null;
route: RouteResult | null; route: RouteResult | null;
navigationActive?: boolean;
guidanceActive?: boolean; guidanceActive?: boolean;
guidanceTarget?: Coordinate | null; guidanceTarget?: Coordinate | null;
followPosition?: boolean;
onFollowPositionChange?: (follow: boolean) => void;
anchorPoint?: Coordinate | null; anchorPoint?: Coordinate | null;
anchorAlarmRadiusM?: number | null; anchorAlarmRadiusM?: number | null;
anchorWatchActive?: boolean; anchorWatchActive?: boolean;
@@ -71,8 +74,11 @@ export function MapView({
waypoints = [], waypoints = [],
pickMode, pickMode,
route, route,
navigationActive = false,
guidanceActive = false, guidanceActive = false,
guidanceTarget = null, guidanceTarget = null,
followPosition = false,
onFollowPositionChange,
anchorPoint = null, anchorPoint = null,
anchorAlarmRadiusM = null, anchorAlarmRadiusM = null,
anchorWatchActive = false, anchorWatchActive = false,
@@ -98,6 +104,7 @@ export function MapView({
const accuracyMRef = useRef(accuracyM); const accuracyMRef = useRef(accuracyM);
const guidanceActiveRef = useRef(guidanceActive); const guidanceActiveRef = useRef(guidanceActive);
const guidanceTargetRef = useRef(guidanceTarget); const guidanceTargetRef = useRef(guidanceTarget);
const onFollowPositionChangeRef = useRef(onFollowPositionChange);
const anchorPointRef = useRef(anchorPoint); const anchorPointRef = useRef(anchorPoint);
const anchorAlarmRadiusMRef = useRef(anchorAlarmRadiusM); const anchorAlarmRadiusMRef = useRef(anchorAlarmRadiusM);
const anchorWatchActiveRef = useRef(anchorWatchActive); const anchorWatchActiveRef = useRef(anchorWatchActive);
@@ -114,6 +121,10 @@ export function MapView({
applyMapFocusRequest(mapRef.current, focusRequest); applyMapFocusRequest(mapRef.current, focusRequest);
}, [focusRequest]); }, [focusRequest]);
useEffect(() => {
onFollowPositionChangeRef.current = onFollowPositionChange;
}, [onFollowPositionChange]);
const clearContactFeatures = useCallback((map: MapLibreMap) => { const clearContactFeatures = useCallback((map: MapLibreMap) => {
const hadContactFeatures = contactFeaturesRef.current.length > 0; const hadContactFeatures = contactFeaturesRef.current.length > 0;
contactFeaturesRef.current = []; contactFeaturesRef.current = [];
@@ -174,6 +185,9 @@ export function MapView({
map.addControl(new maplibregl.AttributionControl({ compact: true }), "bottom-right"); map.addControl(new maplibregl.AttributionControl({ compact: true }), "bottom-right");
map.addControl(new maplibregl.ScaleControl({ unit: "nautical" }), "bottom-left"); map.addControl(new maplibregl.ScaleControl({ unit: "nautical" }), "bottom-left");
map.on("dragstart", () => {
onFollowPositionChangeRef.current?.(false);
});
mapRef.current = map; mapRef.current = map;
map.on("load", () => { map.on("load", () => {
@@ -937,7 +951,15 @@ export function MapView({
const source = map.getSource("user-position") as GeoJSONSource | undefined; const source = map.getSource("user-position") as GeoJSONSource | undefined;
source?.setData(userPositionFeatures(position, accuracyM)); 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; hasCenteredOnFirstFixRef.current = true;
map.easeTo({ map.easeTo({
center: [position.lon, position.lat], center: [position.lon, position.lat],
@@ -946,7 +968,7 @@ export function MapView({
essential: true essential: true
}); });
} }
}, [accuracyM, position]); }, [accuracyM, followPosition, position]);
useEffect(() => { useEffect(() => {
const map = mapRef.current; const map = mapRef.current;
@@ -1075,6 +1097,7 @@ export function MapView({
data-pick-mode={pickMode ?? "none"} data-pick-mode={pickMode ?? "none"}
data-has-route={Boolean(route)} data-has-route={Boolean(route)}
data-guidance-active={guidanceActive} data-guidance-active={guidanceActive}
data-follow-position={followPosition}
> >
<div ref={containerRef} className="map-container" data-testid="map-container" /> <div ref={containerRef} className="map-container" data-testid="map-container" />
{pickMode && ( {pickMode && (
@@ -1101,9 +1124,43 @@ export function MapView({
<button <button
className="map-tool locate-toggle" className="map-tool locate-toggle"
type="button" type="button"
title="Position zentrieren" title={
aria-label="Position zentrieren" navigationActive
onClick={() => mapRef.current?.flyTo({ center: [position.lon, position.lat], zoom: 13 })} ? 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" /> <LocateFixed size={18} aria-hidden="true" />
</button> </button>
+6
View File
@@ -455,6 +455,12 @@ button:disabled {
right: 62px; right: 62px;
} }
.locate-toggle[data-active="true"] {
background: #0f4c5c;
color: #ffffff;
border-color: rgba(255, 255, 255, 0.55);
}
.anchor-toggle { .anchor-toggle {
z-index: 10; z-index: 10;
top: calc(env(safe-area-inset-top) + 116px); 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); 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", () => { describe("MapView anchor watch", () => {