diff --git a/src/app/editor/[id]/page.tsx b/src/app/editor/[id]/page.tsx
index a673339..24c2c23 100644
--- a/src/app/editor/[id]/page.tsx
+++ b/src/app/editor/[id]/page.tsx
@@ -15,7 +15,6 @@ import WikiSidebarPanel from "@/uhm/components/wiki/WikiSidebarPanel";
import ProjectEntityRefsPanel from "@/uhm/components/editor/ProjectEntityRefsPanel";
import EntityWikiBindingsPanel from "@/uhm/components/editor/EntityWikiBindingsPanel";
import GeometryBindingPanel from "@/uhm/components/editor/GeometryBindingPanel";
-import ImageOverlayPanel from "@/uhm/components/editor/ImageOverlayPanel";
import { Entity, fetchEntities, searchEntitiesByName } from "@/uhm/api/entities";
import { ApiError } from "@/uhm/api/http";
import { fetchCurrentUser } from "@/uhm/api/auth";
@@ -2781,15 +2780,6 @@ function EditorPageContent() {
isGeoSearching={isGeoSearching}
onImportGeoFromSearch={handleImportGeoFromSearch}
/>
-
("idle");
const [selectedReplayStageId, setSelectedReplayStageId] = useState(null);
const [selectedReplayStepIndex, setSelectedReplayStepIndex] = useState(null);
+ const [focusedPresentPlace, setFocusedPresentPlace] = useState(null);
const [searchTimelineYear, setSearchTimelineYear] = useState(timelineYear);
useEffect(() => {
@@ -102,19 +109,22 @@ export default function Page() {
return null;
}, [replays, selectedFeatureIds]);
+ const getMapInstance = useCallback(() => mapHandleRef.current?.getMap() || null, []);
+ const handleSelectReplayStep = useCallback((stageId: number | null, stepIndex: number | null) => {
+ setSelectedReplayStageId(stageId);
+ setSelectedReplayStepIndex(stepIndex);
+ }, []);
+
const replayPreview = useReplayPreview({
replay: activeReplay?.replay || null,
draft: renderDraft,
- getMapInstance: () => mapHandleRef.current?.getMap() || null,
+ getMapInstance,
initialTimelineYear: timelineDraftYear,
initialTimelineFilterEnabled: false,
initialMapViewState: null,
selectedStageId: selectedReplayStageId,
selectedStepIndex: selectedReplayStepIndex,
- onSelectStep: (stageId, stepIndex) => {
- setSelectedReplayStageId(stageId);
- setSelectedReplayStepIndex(stepIndex);
- },
+ onSelectStep: handleSelectReplayStep,
});
const {
@@ -205,8 +215,60 @@ export default function Page() {
const handleExitReplay = useCallback(() => {
setReplayMode("idle");
replayPreview.resetPreview();
+ setFocusedPresentPlace(null);
}, [replayPreview]);
+ const handleFocusPresentPlace = useCallback((place: PresentPlaceSelection) => {
+ setFocusedPresentPlace(place);
+ const map = mapHandleRef.current?.getMap();
+ if (map) {
+ const currentZoom = map.getZoom();
+ map.flyTo({
+ center: [place.lng, place.lat],
+ zoom: Math.max(currentZoom, 13.5),
+ });
+ }
+ }, []);
+
+ const clearPresentPlaceFocus = useCallback(() => {
+ setFocusedPresentPlace(null);
+ }, []);
+
+ const handleFocusHistoricalGeometry = useCallback((payload: HistoricalGeometryFocusPayload) => {
+ setFocusedPresentPlace(null);
+
+ const map = mapHandleRef.current?.getMap();
+ if (map && payload.geometry?.draw_geometry) {
+ const fc: FeatureCollection = {
+ type: "FeatureCollection",
+ features: [
+ {
+ type: "Feature",
+ properties: {
+ id: payload.geometry.id,
+ },
+ geometry: payload.geometry.draw_geometry,
+ },
+ ],
+ };
+ fitMapToFeatureCollection(map, fc, 84, { duration: 1000 });
+ }
+
+ if (payload.geometry.time_start != null) {
+ handleTimelineYearChange(payload.geometry.time_start);
+ }
+
+ setSelectedFeatureIds([payload.geometry.id]);
+
+ const linkedEntityIds = relations.geometryEntityIds[String(payload.geometry.id)] || [];
+ if (linkedEntityIds.length === 1) {
+ selectEntity(linkedEntityIds[0], {
+ sourceFeatureId: payload.geometry.id,
+ selectGeometry: false,
+ });
+ }
+ }, [relations.geometryEntityIds, selectEntity, setSelectedFeatureIds]);
+
const filteredRenderDraft = useMemo(() => {
if (replayMode !== "playing" || !replayPreview.hiddenGeometryIds?.length) {
return renderDraft;
@@ -294,7 +356,80 @@ export default function Page() {
/>
) : null
}
- />
+ >
+
+
+
+
+
+
) : (
)}
diff --git a/src/uhm/components/editor/PresentPlaceSearch.tsx b/src/uhm/components/editor/PresentPlaceSearch.tsx
index 9a02372..c44a29a 100644
--- a/src/uhm/components/editor/PresentPlaceSearch.tsx
+++ b/src/uhm/components/editor/PresentPlaceSearch.tsx
@@ -35,6 +35,7 @@ type Props = {
onFocusHistoricalGeometry: (payload: HistoricalGeometryFocusPayload) => void;
onClearFocus: () => void;
leftOffset?: number;
+ style?: CSSProperties;
};
export default function PresentPlaceSearch({
@@ -43,6 +44,7 @@ export default function PresentPlaceSearch({
onFocusHistoricalGeometry,
onClearFocus,
leftOffset = 18,
+ style,
}: Props) {
const [mode, setMode] = useState("present");
const [query, setQuery] = useState("");
@@ -284,6 +286,7 @@ export default function PresentPlaceSearch({
zIndex: 18,
width: "min(392px, calc(100vw - 36px))",
pointerEvents: "auto",
+ ...style,
}}
onMouseDown={(event) => event.stopPropagation()}
>
diff --git a/src/uhm/lib/replay/useReplayPreview.ts b/src/uhm/lib/replay/useReplayPreview.ts
index bc9c85f..477d0b1 100644
--- a/src/uhm/lib/replay/useReplayPreview.ts
+++ b/src/uhm/lib/replay/useReplayPreview.ts
@@ -93,6 +93,13 @@ export function useReplayPreview({
const baselineRef = useRef(null);
const effects = useMemo(() => createReplayMapEffects(), []);
+ const selectedStageIdRef = useRef(selectedStageId);
+ const selectedStepIndexRef = useRef(selectedStepIndex);
+ useEffect(() => {
+ selectedStageIdRef.current = selectedStageId;
+ selectedStepIndexRef.current = selectedStepIndex;
+ }, [selectedStageId, selectedStepIndex]);
+
const flatSteps = useMemo(() => flattenReplaySteps(replay), [replay]);
useEffect(() => {
@@ -203,61 +210,60 @@ export function useReplayPreview({
}, [restorePreviewState]);
const controllersRef = useRef[0] | null>(null);
- useEffect(() => {
- controllersRef.current = {
- map: getMapInstance(),
- draft,
- effects,
- setTimelineVisible,
- setTimelineFilterEnabled,
- setLayerPanelVisible,
- setZoomPanelVisible,
- setSidebarOpen,
- onSelectWiki: (id) => {
- const nextId = String(id || "").trim();
- setActiveWikiId(nextId || null);
- },
- addToast,
- setPlaybackSpeed: (nextSpeed) => {
- const safe = Number.isFinite(nextSpeed) && nextSpeed > 0 ? nextSpeed : 1;
- playbackSpeedRef.current = safe;
- setPlaybackSpeed(safe);
- },
- onYearChange: setTimelineYear,
- showGeometries: (ids) => {
- const nextIds = normalizeIdList(ids);
- if (!nextIds.length) return;
- setHiddenGeometryIds((prev) => prev.filter((id) => !nextIds.includes(id)));
- },
- hideGeometries: (ids) => {
- const nextIds = normalizeIdList(ids);
- if (!nextIds.length) return;
- setHiddenGeometryIds((prev) => {
- const seen = new Set(prev);
- for (const id of nextIds) {
- seen.add(id);
- }
- return Array.from(seen);
- });
- },
- showOnlyGeometries: (ids) => {
- const keepIds = new Set(normalizeIdList(ids));
- if (!keepIds.size) return;
- setHiddenGeometryIds(
- draft.features
- .map((feature) => String(feature.properties.id))
- .filter((id) => !keepIds.has(id))
- );
- },
- showAllGeometries: () => {
- setHiddenGeometryIds([]);
- },
- setDialog: setDialogWithRef,
- getDialog: () => dialogRef.current,
- };
- }, [addToast, draft, effects, getMapInstance]);
+ controllersRef.current = {
+ map: getMapInstance(),
+ draft,
+ effects,
+ setTimelineVisible,
+ setTimelineFilterEnabled,
+ setLayerPanelVisible,
+ setZoomPanelVisible,
+ setSidebarOpen,
+ onSelectWiki: (id) => {
+ const nextId = String(id || "").trim();
+ setActiveWikiId(nextId || null);
+ },
+ addToast,
+ setPlaybackSpeed: (nextSpeed) => {
+ const safe = Number.isFinite(nextSpeed) && nextSpeed > 0 ? nextSpeed : 1;
+ playbackSpeedRef.current = safe;
+ setPlaybackSpeed(safe);
+ },
+ onYearChange: setTimelineYear,
+ showGeometries: (ids) => {
+ const nextIds = normalizeIdList(ids);
+ if (!nextIds.length) return;
+ setHiddenGeometryIds((prev) => prev.filter((id) => !nextIds.includes(id)));
+ },
+ hideGeometries: (ids) => {
+ const nextIds = normalizeIdList(ids);
+ if (!nextIds.length) return;
+ setHiddenGeometryIds((prev) => {
+ const seen = new Set(prev);
+ for (const id of nextIds) {
+ seen.add(id);
+ }
+ return Array.from(seen);
+ });
+ },
+ showOnlyGeometries: (ids) => {
+ const keepIds = new Set(normalizeIdList(ids));
+ if (!keepIds.size) return;
+ setHiddenGeometryIds(
+ draft.features
+ .map((feature) => String(feature.properties.id))
+ .filter((id) => !keepIds.has(id))
+ );
+ },
+ showAllGeometries: () => {
+ setHiddenGeometryIds([]);
+ },
+ setDialog: setDialogWithRef,
+ getDialog: () => dialogRef.current,
+ };
const playFromIndex = useCallback(async (startIndex: number) => {
+ console.log("playFromIndex starting at:", startIndex, "flatSteps count:", flatSteps.length);
if (!flatSteps.length) return;
const safeStartIndex = Math.max(0, Math.min(flatSteps.length - 1, startIndex));
resetPresentation();
@@ -271,7 +277,10 @@ export function useReplayPreview({
setIsPlaying(true);
for (let index = safeStartIndex; index < flatSteps.length; index += 1) {
- if (runIdRef.current !== runId) return;
+ if (runIdRef.current !== runId) {
+ console.log("playFromIndex loop aborted because runId changed");
+ return;
+ }
const current = flatSteps[index];
setActiveCursor({
@@ -282,7 +291,10 @@ export function useReplayPreview({
onSelectStep(current.stageId, current.stepIndex);
const controllers = controllersRef.current;
- if (!controllers) return;
+ if (!controllers) {
+ console.warn("playFromIndex aborted: controllersRef.current is null!");
+ return;
+ }
controllers.map = getMapInstance();
controllers.draft = draft;
@@ -322,9 +334,10 @@ export function useReplayPreview({
}, [playFromIndex]);
const playFromSelection = useCallback(() => {
- const selectedIndex = findReplayStepIndex(flatSteps, selectedStageId, selectedStepIndex);
+ const selectedIndex = findReplayStepIndex(flatSteps, selectedStageIdRef.current, selectedStepIndexRef.current);
+ console.log("playFromSelection called: selectedIndex =", selectedIndex, "selectedStageId =", selectedStageIdRef.current, "selectedStepIndex =", selectedStepIndexRef.current);
void playFromIndex(selectedIndex >= 0 ? selectedIndex : 0);
- }, [flatSteps, playFromIndex, selectedStageId, selectedStepIndex]);
+ }, [flatSteps, playFromIndex]);
return {
isPlaying,