"use client"; 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"; import { BackgroundLayerVisibility } from "@/uhm/lib/map/styles/backgroundLayers"; import type { EditorMode } from "@/uhm/lib/editor/session/sessionTypes"; import { useMapInstance } from "./map/useMapInstance"; import { setupMapLayers } from "./map/useMapLayers"; import { useMapInteraction } from "./map/useMapInteraction"; import { useMapSync } from "./map/useMapSync"; export type MapHoverPayload = { featureId: string | number; feature: Feature | null; point: { x: number; y: number }; 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; backgroundVisibility: BackgroundLayerVisibility; geometryVisibility?: Record; selectedFeatureIds: (string | number)[]; onSelectFeatureIds: (ids: (string | number)[]) => void; onSetMode?: (mode: EditorMode, featureId?: string | number) => void; labelContextDraft?: FeatureCollection; onCreateFeature?: (feature: FeatureCollection["features"][number]) => void; onDeleteFeature?: (id: string | number) => void; onUpdateFeature?: (id: string | number, geometry: Geometry) => void; allowGeometryEditing?: boolean; respectBindingFilter?: boolean; height?: CSSProperties["height"]; fitToDraftBounds?: boolean; fitBoundsKey?: string | number | null; onHoverFeatureChange?: ((payload: MapHoverPayload | null) => void) | undefined; highlightFeatures?: FeatureCollection | null; focusFeatureCollection?: FeatureCollection | null; focusRequestKey?: string | number | null; focusPadding?: number | import("maplibre-gl").PaddingOptions; hideOutside?: boolean; onToggleHideOutside?: () => void; onUndoReplay?: () => void; canUndoReplay?: boolean; }; const Map = forwardRef(function Map({ mode, onSetMode, draft, backgroundVisibility, geometryVisibility, selectedFeatureIds, onSelectFeatureIds, labelContextDraft, onCreateFeature, onDeleteFeature, onUpdateFeature, allowGeometryEditing = true, respectBindingFilter = true, height = "100vh", fitToDraftBounds = false, fitBoundsKey = null, onHoverFeatureChange, highlightFeatures = null, focusFeatureCollection = null, focusRequestKey = null, focusPadding, hideOutside = false, onToggleHideOutside, onUndoReplay, canUndoReplay = false, }, ref) { const modeRef = useRef(mode); const draftRef = useRef(draft); const onSelectFeatureIdsRef = useRef(onSelectFeatureIds); const onSetModeRef = useRef(onSetMode); const onHoverFeatureChangeRef = useRef(onHoverFeatureChange); const onCreateRef = useRef(onCreateFeature); const onDeleteRef = useRef(onDeleteFeature); const onUpdateRef = useRef(onUpdateFeature); useEffect(() => { modeRef.current = mode; }, [mode]); useEffect(() => { draftRef.current = draft; }, [draft]); useEffect(() => { onSelectFeatureIdsRef.current = onSelectFeatureIds; }, [onSelectFeatureIds]); useEffect(() => { onSetModeRef.current = onSetMode; }, [onSetMode]); useEffect(() => { onHoverFeatureChangeRef.current = onHoverFeatureChange; }, [onHoverFeatureChange]); useEffect(() => { onCreateRef.current = onCreateFeature; }, [onCreateFeature]); useEffect(() => { onDeleteRef.current = onDeleteFeature; }, [onDeleteFeature]); useEffect(() => { onUpdateRef.current = onUpdateFeature; }, [onUpdateFeature]); const { mapRef, containerRef, fatalInitError, zoomLevel, zoomBounds, isGlobeProjection, setIsGlobeProjection, isMapLoaded, 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, cleanupMapInteractions, } = useMapInteraction({ mapRef, mode, modeRef, draftRef, allowGeometryEditing, selectedFeatureIds, onSelectFeatureIdsRef, onSetModeRef, onCreateRef, onDeleteRef, onUpdateRef, onHoverFeatureChangeRef, }); const { applyDraftToMap, applyHighlightToMap, tryCenterToUserLocation, } = useMapSync({ mapRef, draft, labelContextDraft, backgroundVisibility, geometryVisibility, selectedFeatureIds, respectBindingFilter, fitToDraftBounds, fitBoundsKey, highlightFeatures, focusFeatureCollection, focusRequestKey, focusPadding, allowGeometryEditing, editingEngineRef, geolocationCenteredRef, }); useEffect(() => { const map = mapRef.current; if (!map || !isMapLoaded) return; setupMapLayers(map, backgroundVisibility, highlightFeatures, applyHighlightToMap); setupMapInteractions(map); applyDraftToMap(draftRef.current); tryCenterToUserLocation(); return () => { cleanupMapInteractions(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [isMapLoaded]); useEffect(() => { const map = mapRef.current; if (map && isMapLoaded) { // Trigger resize after a short delay to allow layout to settle setTimeout(() => map.resize(), 100); } }, [mode, isMapLoaded, mapRef]); return (
{fatalInitError ? (
Map khong khoi tao duoc
{fatalInitError}
) : null}
{mode === "replay" && ( <>
Hide Outside
)} handleZoomSliderChange(Number(event.target.value))} style={{ flex: 1, accentColor: "#38bdf8", cursor: "pointer", }} aria-label="Map zoom" />
{zoomLevel.toFixed(1)}x
); }); export default Map; const zoomButtonStyle: React.CSSProperties = { width: "28px", height: "28px", borderRadius: "999px", border: "1px solid #334155", background: "#1e293b", color: "#f8fafc", fontSize: "18px", lineHeight: "1", cursor: "pointer", display: "grid", placeItems: "center", };