feat: implement replay system with action dispatchers and context switching between main and playback modes
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { type CSSProperties, useEffect, useRef } from "react";
|
||||
import { type CSSProperties, useEffect, useRef, forwardRef, useImperativeHandle, useCallback } from "react";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
|
||||
import { Feature, FeatureCollection, Geometry } from "@/uhm/lib/editor/state/useEditorState";
|
||||
@@ -19,6 +19,16 @@ export type MapHoverPayload = {
|
||||
lngLat: { lng: number; lat: number };
|
||||
};
|
||||
|
||||
export type MapHandle = {
|
||||
getViewState: () => {
|
||||
center: { lng: number; lat: number };
|
||||
zoom: number;
|
||||
pitch: number;
|
||||
bearing: number;
|
||||
projection: string;
|
||||
} | null;
|
||||
};
|
||||
|
||||
type MapProps = {
|
||||
mode: EditorMode;
|
||||
draft: FeatureCollection;
|
||||
@@ -45,7 +55,7 @@ type MapProps = {
|
||||
onToggleHideOutside?: () => void;
|
||||
};
|
||||
|
||||
export default function Map({
|
||||
const Map = forwardRef<MapHandle, MapProps>(function Map({
|
||||
mode,
|
||||
onSetMode,
|
||||
draft,
|
||||
@@ -69,7 +79,7 @@ export default function Map({
|
||||
focusPadding,
|
||||
hideOutside = false,
|
||||
onToggleHideOutside,
|
||||
}: MapProps) {
|
||||
}, ref) {
|
||||
const modeRef = useRef<MapProps["mode"]>(mode);
|
||||
const draftRef = useRef<FeatureCollection>(draft);
|
||||
const onSelectFeatureIdsRef = useRef(onSelectFeatureIds);
|
||||
@@ -100,8 +110,21 @@ export default function Map({
|
||||
geolocationCenteredRef,
|
||||
handleZoomByStep,
|
||||
handleZoomSliderChange,
|
||||
getViewState,
|
||||
} = useMapInstance();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getViewState,
|
||||
}), [getViewState]);
|
||||
|
||||
const handleLogViewState = useCallback(() => {
|
||||
const state = getViewState();
|
||||
console.log("Current Map View State:", state);
|
||||
if (state) {
|
||||
alert(`Captured View State:\nCenter: ${state.center.lng.toFixed(4)}, ${state.center.lat.toFixed(4)}\nZoom: ${state.zoom.toFixed(2)}\nPitch: ${state.pitch.toFixed(1)}°\nBearing: ${state.bearing.toFixed(1)}°\nProjection: ${state.projection}`);
|
||||
}
|
||||
}, [getViewState]);
|
||||
|
||||
const {
|
||||
editingEngineRef,
|
||||
setupMapInteractions,
|
||||
@@ -251,6 +274,27 @@ export default function Map({
|
||||
Thoát Replay Edit
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLogViewState}
|
||||
title="Capture current map view state"
|
||||
style={{
|
||||
...zoomButtonStyle,
|
||||
width: "auto",
|
||||
padding: "0 12px",
|
||||
fontSize: "12px",
|
||||
fontWeight: 700,
|
||||
background: "#1e293b",
|
||||
color: "#38bdf8",
|
||||
border: "1px solid #334155",
|
||||
borderRadius: "999px",
|
||||
cursor: "pointer",
|
||||
marginRight: "8px",
|
||||
}}
|
||||
>
|
||||
Capture View
|
||||
</button>
|
||||
|
||||
<div
|
||||
onClick={onToggleHideOutside}
|
||||
style={{
|
||||
@@ -406,7 +450,9 @@ export default function Map({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default Map;
|
||||
|
||||
const zoomButtonStyle: React.CSSProperties = {
|
||||
width: "28px",
|
||||
|
||||
@@ -20,6 +20,7 @@ type Props = {
|
||||
isEntitySubmitting: boolean;
|
||||
onApplyGeometryMetadata: () => Promise<{ ok: boolean; error?: string }>;
|
||||
changeCount: number;
|
||||
onReplayEdit?: (id: string | number) => void;
|
||||
};
|
||||
|
||||
export default function SelectedGeometryPanel({
|
||||
@@ -30,6 +31,7 @@ export default function SelectedGeometryPanel({
|
||||
isEntitySubmitting,
|
||||
onApplyGeometryMetadata,
|
||||
changeCount,
|
||||
onReplayEdit,
|
||||
}: Props) {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [geoApplyFeedback, setGeoApplyFeedback] = useState<
|
||||
@@ -201,6 +203,20 @@ export default function SelectedGeometryPanel({
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
{onReplayEdit && selectedFeatures.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onReplayEdit(selectedFeatures[0].properties.id)}
|
||||
style={{
|
||||
...primaryGeometryButtonStyle,
|
||||
background: "#1e293b",
|
||||
border: "1px solid #334155",
|
||||
color: "#38bdf8",
|
||||
}}
|
||||
>
|
||||
Replay Edit
|
||||
</button>
|
||||
)}
|
||||
{visibleGeoApplyFeedback ? (
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -125,6 +125,20 @@ export function useMapInstance() {
|
||||
setZoomLevel(next);
|
||||
}, [zoomBounds]);
|
||||
|
||||
const getViewState = useCallback(() => {
|
||||
const map = mapRef.current;
|
||||
if (!map) return null;
|
||||
const center = map.getCenter();
|
||||
const projection = map.getProjection();
|
||||
return {
|
||||
center: { lng: center.lng, lat: center.lat },
|
||||
zoom: map.getZoom(),
|
||||
pitch: map.getPitch(),
|
||||
bearing: map.getBearing(),
|
||||
projection: String(projection?.type || "mercator"),
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
mapRef,
|
||||
containerRef,
|
||||
@@ -138,5 +152,6 @@ export function useMapInstance() {
|
||||
geolocationCenteredRef,
|
||||
handleZoomByStep,
|
||||
handleZoomSliderChange,
|
||||
getViewState,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -64,6 +64,8 @@ export function useMapInteraction({
|
||||
useEffect(() => {
|
||||
if (mode !== "select" || !selectedFeatureIds || selectedFeatureIds.length === 0) {
|
||||
editingEngineRef.current?.clearEditing();
|
||||
// Clear the internal selection state of the select engine to stay in sync with React state
|
||||
engineBindingsRef.current.select?.clearSelection?.(false);
|
||||
}
|
||||
}, [mode, selectedFeatureIds]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user