Verschiebemodus eingebaut
This commit is contained in:
@@ -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"}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user